diff --git a/plugins/portuguese/novelmania.ts b/plugins/portuguese/novelmania.ts index d3cda7130..e8b6baf80 100644 --- a/plugins/portuguese/novelmania.ts +++ b/plugins/portuguese/novelmania.ts @@ -1,186 +1,383 @@ import { fetchApi } from '@libs/fetch'; import { Plugin } from '@/types/plugin'; import { Filters, FilterTypes } from '@libs/filterInputs'; -import { load as parseHTML } from 'cheerio'; import { defaultCover } from '@libs/defaultCover'; import { NovelStatus } from '@libs/novelStatus'; +const BASE = 'https://novelmania.com.br'; +const API = `${BASE}/api`; + +const JSON_HEADERS = { + Accept: 'application/json', + 'Content-Type': 'application/json', +}; + +// Category IDs from GET /api/categories +// Format: base64(numeric_id)--hmac_signature +const CATEGORY_IDS: Record = { + acao: 'MQ==--5a499ef3b8d91aa3cb65aa53fb8c3b9e7dc63491', + adulto: 'Mg==--3edd6783406bb8ea6fea9e69ee1a53221f8c8677', + antologia: 'Mzk=--fc9d86ba0817da6aeaa6f0b319d42e2ebf06d3fb', + 'artes-marciais': 'Nw==--ebf238927942e0d5b38f24a3c0c010834cc18831', + aventura: 'Mw==--dc8f9159e95541b096fddfac225e9771f62f38d8', + comedia: 'NA==--a98f1ec98d64ebbcdcd5476d6ad25077599d33c7', + conto: 'Mzg=--e76a0af4b1a83a49d1de0d7d3c389c411900e1f8', + cotidiano: 'MTY=--9fc70df7bd24b284fe9e520288ce69447511897c', + cultivo: 'NDc=--24a31ae8bda1c325f79008b3790b47ca7db9102d', + distopia: 'NDE=--938e9afa59a514e61dd3d6cf05d17b26e98f25bb', + drama: 'MjM=--720b847dc3393c8980802bf669350f9cd1820c88', + ecchi: 'Mjc=--dd505aa865dde3bfe59b30354521d36af4cd4f45', + erotico: 'MjI=--6e33800803a227ee79310f4f96ae9aa64a96963b', + escolar: 'MTM=--96483791476f3c5fa2eb3d1f404e653a7bdb2e81', + esporte: 'NDg=--f93748b5312e976a729672c2d13684e11fc8c374', + exploracao: 'NDU=--8456f9b64784d8948d3e5f88ed47fd852a3d9e53', + fantasia: 'NQ==--dee563c44dc609497fb77efa31e87790f2df9790', + futurista: 'NDA=--7e196d30b7e0ebc56c2262c87ffc679ce67027aa', + harem: 'MjE=--ca090876e2b3fda7071d4ecfb0bf20661d41521d', + historico: 'NDI=--646e80a24ed359541c640ddda9bfe888aa19e952', + horror: 'NDM=--3ddda0141eb10fff8d52ed336c7c1cf93991905c', + isekai: 'MzA=--6fc21dc13a5bb7052d027e368b4c0f3c621713fc', + magia: 'MjY=--af3fa7514514c8310ed4ac7cf5d5968f369f945f', + mecha: 'OA==--ae005d85f3acff878acb0a42e082ba3f789006fc', + medieval: 'MzE=--1ff340b6de4b5705107068d3bc7e59a421e7c023', + militar: 'MjQ=--f762e35f847a412c4d26bc6771c6de837d2e454a', + misterio: 'OQ==--6a6ecd8163ff655c66e909abda637d1f9156637f', + mitologia: 'MTA=--ea1dc1d655cd26fdccd855ed18556b15bd46df3a', + psicologico: 'MTE=--257277ff50641ff1b449c8ddc49827add51b3688', + punk: 'NDQ=--3c2f2e3cd85d1bf7108efc68baeb5b7aa5dcd771', + 'realidade-virtual': 'MzY=--e63e54c8a22b1c7615caae64bb2a94696bd4ce9e', + romance: 'MTI=--5c7213eb8fb9755f09d03047d74111ea299660f9', + 'sci-fi': 'MTQ=--92c86eb0f3782f0f72d1df74d3cb6151461bf6fb', + 'sistema-de-jogo': 'MTU=--979dddf096b85ec3a7b308a6c84286403a6b18be', + sobrenatural: 'MTc=--af1341af226303a427debb258b131a0f0dd54136', + 'super-heroi': 'NDY=--872de17b8d32b7932921bc4a4a80ff4d1e801d0d', + suspense: 'Mjk=--ffa46a31e5d1a09c2225ebf8fd7ff94611645916', + terror: 'Ng==--1b3e18488c4b253b9d5e877df512588018f5f756', + wuxia: 'MTg=--61693f1537dcfaf89b20f4b6b32d2f2492d97c4d', + xianxia: 'MTk=--9ee4a5e1c6cc7d95bd38b55076610be92ea737cc', + xuanhuan: 'MjA=--be92102bd756136a9aa5143058b714d1811b2dd7', + yaoi: 'MzU=--ff0c99b1b88c74382e4215b31981d66fea6de07c', + yuri: 'Mzc=--13f00e2914197579c5c3f3794c89ed35f27c7d42', +}; + +function mapStatus(status: string): string { + switch (status) { + case 'Ativo': return NovelStatus.Ongoing; + case 'Completo': return NovelStatus.Completed; + case 'Pausado': return NovelStatus.OnHiatus; + case 'Parado': return NovelStatus.OnHiatus; + default: return NovelStatus.Unknown; + } +} + +/** + * Decode a JavaScript-escaped string from the React SSR $R data stream. + * Handles \x3C, \uXXXX, \", \n, \r, \t, \\ sequences. + */ +function decodeJsString(raw: string): string { + return raw + .replace(/\\x([0-9a-fA-F]{2})/g, (_, h) => String.fromCharCode(parseInt(h, 16))) + .replace(/\\u([0-9a-fA-F]{4})/g, (_, h) => String.fromCharCode(parseInt(h, 16))) + .replace(/\\"/g, '"') + .replace(/\\n/g, '\n') + .replace(/\\r/g, '\r') + .replace(/\\t/g, '\t') + .replace(/\\\\/g, '\\'); +} + +/** + * Decode HTML entities from the novel synopsis (API returns HTML-encoded text). + * Handles numeric entities (&#NNNN; &#xHH;) and the most common named entities + * used in Portuguese text. + */ +function decodeHtmlEntities(text: string): string { + return text + .replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n))) + .replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCharCode(parseInt(h, 16))) + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/ /g, ' ') + // Lowercase accented vowels + .replace(/á/g, 'á').replace(/é/g, 'é').replace(/í/g, 'í') + .replace(/ó/g, 'ó').replace(/ú/g, 'ú') + .replace(/à/g, 'à').replace(/è/g, 'è').replace(/ì/g, 'ì') + .replace(/ò/g, 'ò').replace(/ù/g, 'ù') + .replace(/â/g, 'â').replace(/ê/g, 'ê').replace(/î/g, 'î') + .replace(/ô/g, 'ô').replace(/û/g, 'û') + .replace(/ä/g, 'ä').replace(/ë/g, 'ë').replace(/ï/g, 'ï') + .replace(/ö/g, 'ö').replace(/ü/g, 'ü') + .replace(/ã/g, 'ã').replace(/õ/g, 'õ') + .replace(/ç/g, 'ç').replace(/ñ/g, 'ñ') + // Uppercase accented vowels + .replace(/Á/g, 'Á').replace(/É/g, 'É').replace(/Í/g, 'Í') + .replace(/Ó/g, 'Ó').replace(/Ú/g, 'Ú') + .replace(/Ã/g, 'Ã').replace(/Õ/g, 'Õ') + .replace(/Ç/g, 'Ç') + .replace(/Â/g, 'Â').replace(/Ê/g, 'Ê').replace(/Ô/g, 'Ô') + // Typography + .replace(/…/g, '…').replace(/–/g, '–').replace(/—/g, '—') + .replace(/‘/g, '\u2018').replace(/’/g, '\u2019') + .replace(/“/g, '\u201C').replace(/”/g, '\u201D') + .replace(/•/g, '•').replace(/·/g, '·') + .replace(/™/g, '™').replace(/©/g, '©').replace(/®/g, '®'); +} + +/** + * Convert HTML synopsis (with

tags and HTML entities) to plain text. + * LNReader renders the summary as plain text, so we strip tags and decode entities. + */ +function cleanSynopsis(raw: string): string { + return decodeHtmlEntities(raw) + .replace(//gi, '\n') + .replace(/<\/p>/gi, '\n\n') + .replace(/]*>/gi, '') + .replace(/<[^>]+>/g, '') + .replace(/\r\n/g, '\n') + .replace(/\n{3,}/g, '\n\n') + .trim(); +} + class NovelMania implements Plugin.PluginBase { - id = 'novelmania.com.br'; - name = 'Novel Mania'; - icon = 'src/pt-br/novelmania/icon.png'; - site = 'https://novelmania.com.br'; - version = '1.0.1'; + id = 'novelmania.com.br'; + name = 'Novel Mania'; + icon = 'src/pt-br/novelmania/icon.png'; + site = BASE; + version = '2.0.1'; imageRequestInit?: Plugin.ImageRequestInit | undefined = undefined; async popularNovels( pageNo: number, { filters }: Plugin.PopularNovelsOptions, ): Promise { - let url = `${this.site}/novels?titulo=`; - url += `&categoria=${filters?.genres.value}`; - url += `&status=${filters?.status.value}`; - url += `&nacionalidade=${filters?.type.value}`; - url += `&ordem=${filters?.ordem.value}`; - url += `&page%5Bpage%5D=${pageNo}`; - - const body = await fetchApi(url).then(res => res.text()); - - const loadedCheerio = parseHTML(body); - const load = loadedCheerio('div.top-novels.dark.col-6 > div.row.mb-2'); - - const novels: Plugin.NovelItem[] = load - .map((index, element) => ({ - name: loadedCheerio(element).find('a.novel-title > h5').text(), - cover: loadedCheerio(element) - .find('a > div.card.c-size-1.border > img.card-image') - .attr('src'), - path: loadedCheerio(element).find('a.novel-title').attr('href') || '', - })) - .get() - .filter(novel => novel.name && novel.path); - return novels; + const params = new URLSearchParams(); + params.set('page', String(pageNo)); + + // Category: API requires the internal ID (base64--hmac), not the slug + const genreSlug = filters?.genres.value; + if (genreSlug && CATEGORY_IDS[genreSlug]) { + params.append('categories[]', CATEGORY_IDS[genreSlug]); + } + + // Status: API expects array param statuses[] + const status = filters?.status.value; + if (status) params.append('statuses[]', status); + + // Nationality: API expects array param nationalities[] with capitalized value + const nat = filters?.type.value; + if (nat) params.append('nationalities[]', nat); + + const json = await fetchApi(`${API}/novels?${params}`, { + headers: JSON_HEADERS, + }).then(r => r.json()); + + return (json.data ?? []).map((n: any) => ({ + name: n.title, + cover: n.cover?.large ?? defaultCover, + path: `/novels/${n.slug}`, + })); } + async parseNovel(novelPath: string): Promise { - const body = await fetchApi(this.site + novelPath).then(r => r.text()); - const loadedCheerio = parseHTML(body); - - const novel: Plugin.SourceNovel = { - path: novelPath, - name: - loadedCheerio( - 'div.col-md-8 > div.novel-info > div.d-flex.flex-row.align-items-center > h1', - ) - .text() - .trim() || 'Sem título', - }; + // novelPath is like /novels/avatar-do-rei-ar + const slug = novelPath.split('/').filter(Boolean).pop()!; - loadedCheerio('b').remove(); - - novel.name = - loadedCheerio( - 'div.col-md-8 > div.novel-info > div.d-flex.flex-row.align-items-center > h1', - ) - .text() - .trim() || 'Sem título'; - novel.summary = - loadedCheerio('div.tab-pane.fade.show.active > div.text > p') - .map((i, el) => loadedCheerio(el).text()) - .toArray() - .join('\n\n') - .trim() || ''; - novel.cover = - loadedCheerio('div.novel-img > img.img-responsive').attr('src') || - defaultCover; - novel.author = loadedCheerio('div.novel-info > span.authors.mb-1') - .text() - .trim(); - novel.genres = loadedCheerio('div.tags > ul.list-tags.mb-0 > li > a') - .map((i, el) => loadedCheerio(el).text()) - .toArray() - .join(','); - - const status = loadedCheerio('div.novel-info > span.authors.mb-3') - .text() - .trim(); - switch (status) { - case 'Ativo': - novel.status = NovelStatus.Ongoing; - break; - case 'Pausado': - novel.status = NovelStatus.OnHiatus; - break; - case 'Completo': - novel.status = NovelStatus.Completed; - break; - default: - novel.status = NovelStatus.Unknown; - } + const novelJson = await fetchApi(`${API}/novels/${slug}`, { + headers: JSON_HEADERS, + }).then(r => r.json()); + const n = novelJson.data; + // Collect all chapters — API paginates (20 items per page) const chapters: Plugin.ChapterItem[] = []; + let page = 1; + while (true) { + const chapJson = await fetchApi( + `${API}/novels/${slug}/chapters?page=${page}`, + { headers: JSON_HEADERS }, + ).then(r => r.json()); + + const batch: any[] = chapJson.data ?? []; + if (!batch.length) break; + + for (const ch of batch) { + chapters.push({ + name: ch.longTitle || ch.title, + path: `/novels/${slug}/capitulos/${ch.slug}`, + }); + } + + if (batch.length < 20) break; + page++; + } - loadedCheerio( - 'div.accordion.capitulo > div.card > div.collapse > div.card-body.p-0 > ol > li', - ).each((i, el) => { - const chapterName = `${loadedCheerio(el).find('a > span.sub-vol').text().trim()} - ${loadedCheerio(el).find('a > strong').text().trim()}`; - const chapterPath = loadedCheerio(el).find('a').attr('href'); - if (chapterPath) chapters.push({ name: chapterName, path: chapterPath }); - }); - novel.chapters = chapters; - return novel; + return { + path: novelPath, + name: n.title, + cover: n.cover?.large ?? defaultCover, + summary: cleanSynopsis(n.synopsis ?? ''), + author: n.author ?? '', + genres: (n.categories ?? []).map((c: any) => c.name).join(','), + status: mapStatus(n.status), + chapters, + }; } + async parseChapter(chapterPath: string): Promise { - const response = await fetchApi(`${this.site}${chapterPath}`).then(res => - res.text(), - ); - const loadedCheerio = parseHTML(response); - return loadedCheerio('div#chapter-content').html() || ''; + // chapterPath is like /novels/avatar-do-rei-ar/capitulos/volume-1-capitulo-1 + // The chapter JSON API returns 403. Content and metadata are extracted from + // the React SSR $R data stream embedded in the HTML page. + const html = await fetchApi(`${BASE}${chapterPath}`).then(r => r.text()); + + // --- Extract chapter content --- + const contentMatch = html.match(/[,{]content:"((?:[^"\\]|\\.)*)"/); + const content = contentMatch?.[1] ? decodeJsString(contentMatch[1]) : ''; + + // --- Extract chapter title from SSR stream --- + // The chapter object has: ...,updatedAt:"ISO",title:"TITLE",slug: + // First match in the stream is always the current chapter (not next/prev). + const titleMatch = html.match(/updatedAt:"[^"]+",title:"((?:[^"\\]|\\.)*)",slug:/); + const chapterTitle = titleMatch?.[1] ? decodeJsString(titleMatch[1]) : ''; + + // --- Extract volume name from longTitle ("Volume 1 \u2013 Cap\u00edtulo 1") --- + // \u2013 = em-dash (U+2013); avoid encoding issues by using the code point. + const longTitleMatch = html.match(/longTitle:"((?:[^"\\]|\\.)*)"/); + const longTitle = longTitleMatch?.[1] ? decodeJsString(longTitleMatch[1]) : ''; + let volumeName = longTitle.split(/\s*[\u2013\-]\s*/)[0]?.trim() ?? ''; + + // --- Fallback: derive volume + short chapter label from og:title meta tag --- + // og:title format: "Volume 1 – Capítulo 1 – Novel Name" + let fallbackTitle = ''; + if (!longTitle) { + const ogMatch = html.match(/og:title[^>]*content="([^"]+)"/); + if (ogMatch?.[1]) { + const parts = ogMatch[1].split(/\s*[\u2013\-]\s*/); + if (parts.length >= 2) { + volumeName = parts[0]?.trim() ?? ''; + fallbackTitle = parts[1]?.trim() ?? ''; + } + } + } + + const displayTitle = chapterTitle || fallbackTitle; + + // --- Extract translator usernames from translators:$R[N]=[{username:"X"},...] --- + // Uses exec loop (wider engine compatibility than matchAll). + const translators: string[] = []; + const transIdx = html.indexOf('translators:$R['); + if (transIdx !== -1) { + const chunk = html.slice(transIdx, transIdx + 500); + const reUser = /username:"((?:[^"\\]|\\.)*)"/g; + let mu: RegExpExecArray | null; + while ((mu = reUser.exec(chunk)) !== null) { + const name = decodeJsString(mu[1]); + if (name && !translators.includes(name)) translators.push(name); + } + } + + // --- Build header HTML (mirrors the website layout) --- + const parts: string[] = []; + if (volumeName) { + parts.push( + '

' + + volumeName + '

', + ); + } + if (displayTitle) { + parts.push( + '

' + + displayTitle + '

', + ); + } + if (translators.length > 0) { + parts.push( + '

Tradução: ' + + translators.join(', ') + '

', + ); + } + + const header = parts.length > 0 + ? '
' + parts.join('') + '
' + + '
' + : ''; + + return header + content; } + async searchNovels( searchTerm: string, pageNo: number, ): Promise { - const url = `${this.site}/novels?titulo=${searchTerm}&page%5Bpage%5D=${pageNo}`; - const body = await fetchApi(url).then(res => res.text()); - const loadedCheerio = parseHTML(body); - const load = loadedCheerio('div.top-novels.dark.col-6 > div.row.mb-2'); - - const novels: Plugin.NovelItem[] = load - .map((index, element) => ({ - name: loadedCheerio(element).find('a.novel-title > h5').text(), - cover: loadedCheerio(element) - .find('a > div.card.c-size-1.border > img.card-image') - .attr('src'), - path: loadedCheerio(element).find('a.novel-title').attr('href') || '', - })) - .get() - .filter(novel => novel.name && novel.path); - return novels; - - return novels; + // Search param is "q" (not "titulo") + const params = new URLSearchParams(); + params.set('q', searchTerm); + params.set('page', String(pageNo)); + + const json = await fetchApi(`${API}/novels?${params}`, { + headers: JSON_HEADERS, + }).then(r => r.json()); + + return (json.data ?? []).map((n: any) => ({ + name: n.title, + cover: n.cover?.large ?? defaultCover, + path: `/novels/${n.slug}`, + })); } resolveUrl = (path: string, isNovel?: boolean) => - this.site + (isNovel ? '/book/' : '/chapter/') + path; + isNovel ? `${BASE}${path}` : `${BASE}${path}`; filters = { genres: { value: '', label: 'Gêneros', options: [ - { label: 'Todos', value: '' }, - { label: 'Ação', value: '01' }, - { label: 'Adulto', value: '02' }, - { label: 'Artes Marciais', value: '07' }, - { label: 'Aventura', value: '03' }, - { label: 'Comédia', value: '04' }, - { label: 'Cotidiano', value: '16' }, - { label: 'Drama', value: '23' }, - { label: 'Ecchi', value: '27' }, - { label: 'Erótico', value: '22' }, - { label: 'Escolar', value: '13' }, - { label: 'Fantasia', value: '05' }, - { label: 'Harém', value: '21' }, - { label: 'Isekai', value: '30' }, - { label: 'Magia', value: '26' }, - { label: 'Mecha', value: '08' }, - { label: 'Medieval', value: '31' }, - { label: 'Militar', value: '24' }, - { label: 'Mistério', value: '09' }, - { label: 'Mitologia', value: '10' }, - { label: 'Psicológico', value: '11' }, - { label: 'Realidade Virtual', value: '36' }, - { label: 'Romance', value: '12' }, - { label: 'Sci-fi', value: '14' }, - { label: 'Sistema de Jogo', value: '15' }, - { label: 'Sobrenatural', value: '17' }, - { label: 'Suspense', value: '29' }, - { label: 'Terror', value: '06' }, - { label: 'Wuxia', value: '18' }, - { label: 'Xianxia', value: '19' }, - { label: 'Xuanhuan', value: '20' }, - { label: 'Yaoi', value: '35' }, - { label: 'Yuri', value: '37' }, + { label: 'Todos', value: '' }, + { label: 'Ação', value: 'acao' }, + { label: 'Adulto', value: 'adulto' }, + { label: 'Antologia', value: 'antologia' }, + { label: 'Artes Marciais', value: 'artes-marciais' }, + { label: 'Aventura', value: 'aventura' }, + { label: 'Comédia', value: 'comedia' }, + { label: 'Conto', value: 'conto' }, + { label: 'Cotidiano', value: 'cotidiano' }, + { label: 'Cultivo', value: 'cultivo' }, + { label: 'Distopia', value: 'distopia' }, + { label: 'Drama', value: 'drama' }, + { label: 'Ecchi', value: 'ecchi' }, + { label: 'Erótico', value: 'erotico' }, + { label: 'Escolar', value: 'escolar' }, + { label: 'Esporte', value: 'esporte' }, + { label: 'Exploração', value: 'exploracao' }, + { label: 'Fantasia', value: 'fantasia' }, + { label: 'Futurista', value: 'futurista' }, + { label: 'Harém', value: 'harem' }, + { label: 'Histórico', value: 'historico' }, + { label: 'Horror', value: 'horror' }, + { label: 'Isekai', value: 'isekai' }, + { label: 'Magia', value: 'magia' }, + { label: 'Mecha', value: 'mecha' }, + { label: 'Medieval', value: 'medieval' }, + { label: 'Militar', value: 'militar' }, + { label: 'Mistério', value: 'misterio' }, + { label: 'Mitologia', value: 'mitologia' }, + { label: 'Psicológico', value: 'psicologico' }, + { label: 'Punk', value: 'punk' }, + { label: 'Realidade Virtual', value: 'realidade-virtual' }, + { label: 'Romance', value: 'romance' }, + { label: 'Sci-fi', value: 'sci-fi' }, + { label: 'Sistema de Jogo', value: 'sistema-de-jogo' }, + { label: 'Sobrenatural', value: 'sobrenatural' }, + { label: 'Super-Herói', value: 'super-heroi' }, + { label: 'Suspense', value: 'suspense' }, + { label: 'Terror', value: 'terror' }, + { label: 'Wuxia', value: 'wuxia' }, + { label: 'Xianxia', value: 'xianxia' }, + { label: 'Xuanhuan', value: 'xuanhuan' }, + { label: 'Yaoi', value: 'yaoi' }, + { label: 'Yuri', value: 'yuri' }, ], type: FilterTypes.Picker, }, @@ -188,37 +385,25 @@ class NovelMania implements Plugin.PluginBase { label: 'Status', value: '', options: [ - { label: 'Todos', value: '' }, - { label: 'Ativo', value: 'ativo' }, + { label: 'Todos', value: '' }, + { label: 'Ativo', value: 'Ativo' }, { label: 'Completo', value: 'Completo' }, - { label: 'Pausado', value: 'pausado' }, - { label: 'Parado', value: 'Parado' }, + { label: 'Pausado', value: 'Pausado' }, + { label: 'Parado', value: 'Parado' }, ], type: FilterTypes.Picker, }, type: { - label: 'Type', - value: '', - options: [ - { label: 'Todas', value: '' }, - { label: 'Americana', value: 'americana' }, - { label: 'Angolana', value: 'angolana' }, - { label: 'Brasileira', value: 'brasileira' }, - { label: 'Chinesa', value: 'chinesa' }, - { label: 'Coreana', value: 'coreana' }, - { label: 'Japonesa', value: 'japonesa' }, - ], - type: FilterTypes.Picker, - }, - ordem: { - label: 'Ordenar', + label: 'Tipo / Nacionalidade', value: '', options: [ - { label: 'Qualquer ordem', value: '' }, - { label: 'Ordem alfabética', value: '1' }, - { label: 'Nº de Capítulos', value: '2' }, - { label: 'Popularidade', value: '3' }, - { label: 'Novidades', value: '4' }, + { label: 'Todas', value: '' }, + { label: 'Americana', value: 'Americana' }, + { label: 'Angolana', value: 'Angolana' }, + { label: 'Brasileira', value: 'Brasileira' }, + { label: 'Chinesa', value: 'Chinesa' }, + { label: 'Coreana', value: 'Coreana' }, + { label: 'Japonesa', value: 'Japonesa' }, ], type: FilterTypes.Picker, },