diff --git a/ui/messages/en.json b/ui/messages/en.json index 2779e02..114056b 100644 --- a/ui/messages/en.json +++ b/ui/messages/en.json @@ -302,6 +302,24 @@ "book_detail_range_queuing": "Queuing\u2026", "book_detail_scrape_range": "Scrape range", "book_detail_admin": "Admin", + "book_detail_admin_book_cover": "Book Cover", + "book_detail_admin_chapter_cover": "Chapter Cover", + "book_detail_admin_chapter_n": "Chapter #", + "book_detail_admin_description": "Description", + "book_detail_admin_chapter_names": "Chapter Names", + "book_detail_admin_audio_tts": "Audio TTS", + "book_detail_admin_voice": "Voice", + "book_detail_admin_generate": "Generate", + "book_detail_admin_save_cover": "Save Cover", + "book_detail_admin_saving": "Saving…", + "book_detail_admin_saved": "Saved", + "book_detail_admin_apply": "Apply", + "book_detail_admin_applying": "Applying…", + "book_detail_admin_applied": "Applied", + "book_detail_admin_discard": "Discard", + "book_detail_admin_enqueue_audio": "Enqueue Audio", + "book_detail_admin_cancel_audio": "Cancel", + "book_detail_admin_enqueued": "Enqueued {enqueued}, skipped {skipped}", "book_detail_scraping_progress": "Fetching the first 20 chapters. This page will refresh automatically.", "book_detail_scraping_home": "\u2190 Home", "book_detail_rescrape_book": "Rescrape book", diff --git a/ui/messages/fr.json b/ui/messages/fr.json index 8f48460..a21acb8 100644 --- a/ui/messages/fr.json +++ b/ui/messages/fr.json @@ -302,6 +302,24 @@ "book_detail_range_queuing": "En file d'attente…", "book_detail_scrape_range": "Plage d'extraction", "book_detail_admin": "Admin", + "book_detail_admin_book_cover": "Couverture du livre", + "book_detail_admin_chapter_cover": "Couverture du chapitre", + "book_detail_admin_chapter_n": "Chapitre n°", + "book_detail_admin_description": "Description", + "book_detail_admin_chapter_names": "Noms des chapitres", + "book_detail_admin_audio_tts": "Audio TTS", + "book_detail_admin_voice": "Voix", + "book_detail_admin_generate": "Générer", + "book_detail_admin_save_cover": "Enregistrer la couverture", + "book_detail_admin_saving": "Enregistrement…", + "book_detail_admin_saved": "Enregistré", + "book_detail_admin_apply": "Appliquer", + "book_detail_admin_applying": "Application…", + "book_detail_admin_applied": "Appliqué", + "book_detail_admin_discard": "Ignorer", + "book_detail_admin_enqueue_audio": "Mettre en file audio", + "book_detail_admin_cancel_audio": "Annuler", + "book_detail_admin_enqueued": "{enqueued} en file, {skipped} ignorés", "book_detail_scraping_progress": "Récupération des 20 premiers chapitres. Cette page sera actualisée automatiquement.", "book_detail_scraping_home": "← Accueil", "book_detail_rescrape_book": "Réextraire le livre", diff --git a/ui/messages/id.json b/ui/messages/id.json index 66e3396..ef9b659 100644 --- a/ui/messages/id.json +++ b/ui/messages/id.json @@ -302,6 +302,24 @@ "book_detail_range_queuing": "Mengantri…", "book_detail_scrape_range": "Rentang scrape", "book_detail_admin": "Admin", + "book_detail_admin_book_cover": "Sampul Buku", + "book_detail_admin_chapter_cover": "Sampul Bab", + "book_detail_admin_chapter_n": "Bab #", + "book_detail_admin_description": "Deskripsi", + "book_detail_admin_chapter_names": "Nama Bab", + "book_detail_admin_audio_tts": "Audio TTS", + "book_detail_admin_voice": "Suara", + "book_detail_admin_generate": "Buat", + "book_detail_admin_save_cover": "Simpan Sampul", + "book_detail_admin_saving": "Menyimpan…", + "book_detail_admin_saved": "Tersimpan", + "book_detail_admin_apply": "Terapkan", + "book_detail_admin_applying": "Menerapkan…", + "book_detail_admin_applied": "Diterapkan", + "book_detail_admin_discard": "Buang", + "book_detail_admin_enqueue_audio": "Antre Audio", + "book_detail_admin_cancel_audio": "Batal", + "book_detail_admin_enqueued": "Diantre {enqueued}, dilewati {skipped}", "book_detail_scraping_progress": "Mengambil 20 bab pertama. Halaman ini akan dimuat ulang otomatis.", "book_detail_scraping_home": "← Beranda", "book_detail_rescrape_book": "Scrape ulang buku", diff --git a/ui/messages/pt.json b/ui/messages/pt.json index cbde85f..6775699 100644 --- a/ui/messages/pt.json +++ b/ui/messages/pt.json @@ -302,6 +302,24 @@ "book_detail_range_queuing": "Na fila…", "book_detail_scrape_range": "Intervalo de extração", "book_detail_admin": "Admin", + "book_detail_admin_book_cover": "Capa do Livro", + "book_detail_admin_chapter_cover": "Capa do Capítulo", + "book_detail_admin_chapter_n": "Capítulo nº", + "book_detail_admin_description": "Descrição", + "book_detail_admin_chapter_names": "Nomes dos Capítulos", + "book_detail_admin_audio_tts": "Áudio TTS", + "book_detail_admin_voice": "Voz", + "book_detail_admin_generate": "Gerar", + "book_detail_admin_save_cover": "Salvar Capa", + "book_detail_admin_saving": "Salvando…", + "book_detail_admin_saved": "Salvo", + "book_detail_admin_apply": "Aplicar", + "book_detail_admin_applying": "Aplicando…", + "book_detail_admin_applied": "Aplicado", + "book_detail_admin_discard": "Descartar", + "book_detail_admin_enqueue_audio": "Enfileirar Áudio", + "book_detail_admin_cancel_audio": "Cancelar", + "book_detail_admin_enqueued": "{enqueued} enfileirados, {skipped} ignorados", "book_detail_scraping_progress": "Buscando os primeiros 20 capítulos. Esta página será atualizada automaticamente.", "book_detail_scraping_home": "← Início", "book_detail_rescrape_book": "Reextrair livro", diff --git a/ui/messages/ru.json b/ui/messages/ru.json index f903b23..1fcb8ba 100644 --- a/ui/messages/ru.json +++ b/ui/messages/ru.json @@ -302,6 +302,24 @@ "book_detail_range_queuing": "В очереди…", "book_detail_scrape_range": "Диапазон глав", "book_detail_admin": "Администрирование", + "book_detail_admin_book_cover": "Обложка книги", + "book_detail_admin_chapter_cover": "Обложка главы", + "book_detail_admin_chapter_n": "Глава №", + "book_detail_admin_description": "Описание", + "book_detail_admin_chapter_names": "Названия глав", + "book_detail_admin_audio_tts": "Аудио TTS", + "book_detail_admin_voice": "Голос", + "book_detail_admin_generate": "Сгенерировать", + "book_detail_admin_save_cover": "Сохранить обложку", + "book_detail_admin_saving": "Сохранение…", + "book_detail_admin_saved": "Сохранено", + "book_detail_admin_apply": "Применить", + "book_detail_admin_applying": "Применение…", + "book_detail_admin_applied": "Применено", + "book_detail_admin_discard": "Отменить", + "book_detail_admin_enqueue_audio": "Поставить в очередь", + "book_detail_admin_cancel_audio": "Отмена", + "book_detail_admin_enqueued": "В очереди {enqueued}, пропущено {skipped}", "book_detail_scraping_progress": "Загружаются первые 20 глав. Страница обновится автоматически.", "book_detail_scraping_home": "← На главную", "book_detail_rescrape_book": "Перепарсить книгу", diff --git a/ui/src/lib/components/AudioPlayer.svelte b/ui/src/lib/components/AudioPlayer.svelte index dddcec6..cc75479 100644 --- a/ui/src/lib/components/AudioPlayer.svelte +++ b/ui/src/lib/components/AudioPlayer.svelte @@ -574,7 +574,12 @@ // generation runs and MinIO is populated concurrently. // Skip when enqueued=true to avoid double-generation with the async runner. if (!voice.startsWith('cfai:') && !presignResult.enqueued) { - const qs = new URLSearchParams({ voice, format: 'mp3' }); + // PocketTTS outputs raw WAV — skip the ffmpeg transcode entirely. + // WAV (PCM) is natively supported on all platforms including iOS Safari. + // Kokoro and CF AI output MP3 natively, so keep mp3 for those. + const isPocketTTS = voices.some((v) => v.id === voice && v.engine === 'pocket-tts'); + const format = isPocketTTS ? 'wav' : 'mp3'; + const qs = new URLSearchParams({ voice, format }); const streamUrl = `/api/audio-stream/${slug}/${chapter}?${qs}`; // HEAD probe: check paywall without triggering generation. const headRes = await fetch(streamUrl, { method: 'HEAD' }).catch(() => null); diff --git a/ui/src/lib/paraglide/messages.js b/ui/src/lib/paraglide/messages.js index 83a0d9d..608d20d 100644 --- a/ui/src/lib/paraglide/messages.js +++ b/ui/src/lib/paraglide/messages.js @@ -1,2 +1,4 @@ /* eslint-disable */ -export * from './messages/_index.js' \ No newline at end of file +export * from './messages/_index.js' +// enabling auto-import by exposing all messages as m +export * as m from './messages/_index.js' \ No newline at end of file diff --git a/ui/src/lib/paraglide/messages/_index.js b/ui/src/lib/paraglide/messages/_index.js index ba38425..0ac1c9c 100644 --- a/ui/src/lib/paraglide/messages/_index.js +++ b/ui/src/lib/paraglide/messages/_index.js @@ -279,6 +279,24 @@ export * from './book_detail_to_chapter.js' export * from './book_detail_range_queuing.js' export * from './book_detail_scrape_range.js' export * from './book_detail_admin.js' +export * from './book_detail_admin_book_cover.js' +export * from './book_detail_admin_chapter_cover.js' +export * from './book_detail_admin_chapter_n.js' +export * from './book_detail_admin_description.js' +export * from './book_detail_admin_chapter_names.js' +export * from './book_detail_admin_audio_tts.js' +export * from './book_detail_admin_voice.js' +export * from './book_detail_admin_generate.js' +export * from './book_detail_admin_save_cover.js' +export * from './book_detail_admin_saving.js' +export * from './book_detail_admin_saved.js' +export * from './book_detail_admin_apply.js' +export * from './book_detail_admin_applying.js' +export * from './book_detail_admin_applied.js' +export * from './book_detail_admin_discard.js' +export * from './book_detail_admin_enqueue_audio.js' +export * from './book_detail_admin_cancel_audio.js' +export * from './book_detail_admin_enqueued.js' export * from './book_detail_scraping_progress.js' export * from './book_detail_scraping_home.js' export * from './book_detail_rescrape_book.js' diff --git a/ui/src/lib/paraglide/messages/book_detail_admin_applied.js b/ui/src/lib/paraglide/messages/book_detail_admin_applied.js new file mode 100644 index 0000000..fd1a454 --- /dev/null +++ b/ui/src/lib/paraglide/messages/book_detail_admin_applied.js @@ -0,0 +1,44 @@ +/* eslint-disable */ +import { getLocale, experimentalStaticLocale } from '../runtime.js'; + +/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */ + +/** @typedef {{}} Book_Detail_Admin_AppliedInputs */ + +const en_book_detail_admin_applied = /** @type {(inputs: Book_Detail_Admin_AppliedInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Applied`) +}; + +const ru_book_detail_admin_applied = /** @type {(inputs: Book_Detail_Admin_AppliedInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Применено`) +}; + +const id_book_detail_admin_applied = /** @type {(inputs: Book_Detail_Admin_AppliedInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Diterapkan`) +}; + +const pt_book_detail_admin_applied = /** @type {(inputs: Book_Detail_Admin_AppliedInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Aplicado`) +}; + +const fr_book_detail_admin_applied = /** @type {(inputs: Book_Detail_Admin_AppliedInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Appliqué`) +}; + +/** +* | output | +* | --- | +* | "Applied" | +* +* @param {Book_Detail_Admin_AppliedInputs} inputs +* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options +* @returns {LocalizedString} +*/ +export const book_detail_admin_applied = /** @type {((inputs?: Book_Detail_Admin_AppliedInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata} */ ((inputs = {}, options = {}) => { + const locale = experimentalStaticLocale ?? options.locale ?? getLocale() + if (locale === "en") return en_book_detail_admin_applied(inputs) + if (locale === "ru") return ru_book_detail_admin_applied(inputs) + if (locale === "id") return id_book_detail_admin_applied(inputs) + if (locale === "pt") return pt_book_detail_admin_applied(inputs) + return fr_book_detail_admin_applied(inputs) +}); \ No newline at end of file diff --git a/ui/src/lib/paraglide/messages/book_detail_admin_apply.js b/ui/src/lib/paraglide/messages/book_detail_admin_apply.js new file mode 100644 index 0000000..6a888d1 --- /dev/null +++ b/ui/src/lib/paraglide/messages/book_detail_admin_apply.js @@ -0,0 +1,44 @@ +/* eslint-disable */ +import { getLocale, experimentalStaticLocale } from '../runtime.js'; + +/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */ + +/** @typedef {{}} Book_Detail_Admin_ApplyInputs */ + +const en_book_detail_admin_apply = /** @type {(inputs: Book_Detail_Admin_ApplyInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Apply`) +}; + +const ru_book_detail_admin_apply = /** @type {(inputs: Book_Detail_Admin_ApplyInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Применить`) +}; + +const id_book_detail_admin_apply = /** @type {(inputs: Book_Detail_Admin_ApplyInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Terapkan`) +}; + +const pt_book_detail_admin_apply = /** @type {(inputs: Book_Detail_Admin_ApplyInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Aplicar`) +}; + +const fr_book_detail_admin_apply = /** @type {(inputs: Book_Detail_Admin_ApplyInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Appliquer`) +}; + +/** +* | output | +* | --- | +* | "Apply" | +* +* @param {Book_Detail_Admin_ApplyInputs} inputs +* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options +* @returns {LocalizedString} +*/ +export const book_detail_admin_apply = /** @type {((inputs?: Book_Detail_Admin_ApplyInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata} */ ((inputs = {}, options = {}) => { + const locale = experimentalStaticLocale ?? options.locale ?? getLocale() + if (locale === "en") return en_book_detail_admin_apply(inputs) + if (locale === "ru") return ru_book_detail_admin_apply(inputs) + if (locale === "id") return id_book_detail_admin_apply(inputs) + if (locale === "pt") return pt_book_detail_admin_apply(inputs) + return fr_book_detail_admin_apply(inputs) +}); \ No newline at end of file diff --git a/ui/src/lib/paraglide/messages/book_detail_admin_applying.js b/ui/src/lib/paraglide/messages/book_detail_admin_applying.js new file mode 100644 index 0000000..ebdde73 --- /dev/null +++ b/ui/src/lib/paraglide/messages/book_detail_admin_applying.js @@ -0,0 +1,44 @@ +/* eslint-disable */ +import { getLocale, experimentalStaticLocale } from '../runtime.js'; + +/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */ + +/** @typedef {{}} Book_Detail_Admin_ApplyingInputs */ + +const en_book_detail_admin_applying = /** @type {(inputs: Book_Detail_Admin_ApplyingInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Applying…`) +}; + +const ru_book_detail_admin_applying = /** @type {(inputs: Book_Detail_Admin_ApplyingInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Применение…`) +}; + +const id_book_detail_admin_applying = /** @type {(inputs: Book_Detail_Admin_ApplyingInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Menerapkan…`) +}; + +const pt_book_detail_admin_applying = /** @type {(inputs: Book_Detail_Admin_ApplyingInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Aplicando…`) +}; + +const fr_book_detail_admin_applying = /** @type {(inputs: Book_Detail_Admin_ApplyingInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Application…`) +}; + +/** +* | output | +* | --- | +* | "Applying…" | +* +* @param {Book_Detail_Admin_ApplyingInputs} inputs +* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options +* @returns {LocalizedString} +*/ +export const book_detail_admin_applying = /** @type {((inputs?: Book_Detail_Admin_ApplyingInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata} */ ((inputs = {}, options = {}) => { + const locale = experimentalStaticLocale ?? options.locale ?? getLocale() + if (locale === "en") return en_book_detail_admin_applying(inputs) + if (locale === "ru") return ru_book_detail_admin_applying(inputs) + if (locale === "id") return id_book_detail_admin_applying(inputs) + if (locale === "pt") return pt_book_detail_admin_applying(inputs) + return fr_book_detail_admin_applying(inputs) +}); \ No newline at end of file diff --git a/ui/src/lib/paraglide/messages/book_detail_admin_audio_tts.js b/ui/src/lib/paraglide/messages/book_detail_admin_audio_tts.js new file mode 100644 index 0000000..4d9ff6b --- /dev/null +++ b/ui/src/lib/paraglide/messages/book_detail_admin_audio_tts.js @@ -0,0 +1,44 @@ +/* eslint-disable */ +import { getLocale, experimentalStaticLocale } from '../runtime.js'; + +/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */ + +/** @typedef {{}} Book_Detail_Admin_Audio_TtsInputs */ + +const en_book_detail_admin_audio_tts = /** @type {(inputs: Book_Detail_Admin_Audio_TtsInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Audio TTS`) +}; + +const ru_book_detail_admin_audio_tts = /** @type {(inputs: Book_Detail_Admin_Audio_TtsInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Аудио TTS`) +}; + +const id_book_detail_admin_audio_tts = /** @type {(inputs: Book_Detail_Admin_Audio_TtsInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Audio TTS`) +}; + +const pt_book_detail_admin_audio_tts = /** @type {(inputs: Book_Detail_Admin_Audio_TtsInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Áudio TTS`) +}; + +const fr_book_detail_admin_audio_tts = /** @type {(inputs: Book_Detail_Admin_Audio_TtsInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Audio TTS`) +}; + +/** +* | output | +* | --- | +* | "Audio TTS" | +* +* @param {Book_Detail_Admin_Audio_TtsInputs} inputs +* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options +* @returns {LocalizedString} +*/ +export const book_detail_admin_audio_tts = /** @type {((inputs?: Book_Detail_Admin_Audio_TtsInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata} */ ((inputs = {}, options = {}) => { + const locale = experimentalStaticLocale ?? options.locale ?? getLocale() + if (locale === "en") return en_book_detail_admin_audio_tts(inputs) + if (locale === "ru") return ru_book_detail_admin_audio_tts(inputs) + if (locale === "id") return id_book_detail_admin_audio_tts(inputs) + if (locale === "pt") return pt_book_detail_admin_audio_tts(inputs) + return fr_book_detail_admin_audio_tts(inputs) +}); \ No newline at end of file diff --git a/ui/src/lib/paraglide/messages/book_detail_admin_book_cover.js b/ui/src/lib/paraglide/messages/book_detail_admin_book_cover.js new file mode 100644 index 0000000..69f2a75 --- /dev/null +++ b/ui/src/lib/paraglide/messages/book_detail_admin_book_cover.js @@ -0,0 +1,44 @@ +/* eslint-disable */ +import { getLocale, experimentalStaticLocale } from '../runtime.js'; + +/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */ + +/** @typedef {{}} Book_Detail_Admin_Book_CoverInputs */ + +const en_book_detail_admin_book_cover = /** @type {(inputs: Book_Detail_Admin_Book_CoverInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Book Cover`) +}; + +const ru_book_detail_admin_book_cover = /** @type {(inputs: Book_Detail_Admin_Book_CoverInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Обложка книги`) +}; + +const id_book_detail_admin_book_cover = /** @type {(inputs: Book_Detail_Admin_Book_CoverInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Sampul Buku`) +}; + +const pt_book_detail_admin_book_cover = /** @type {(inputs: Book_Detail_Admin_Book_CoverInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Capa do Livro`) +}; + +const fr_book_detail_admin_book_cover = /** @type {(inputs: Book_Detail_Admin_Book_CoverInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Couverture du livre`) +}; + +/** +* | output | +* | --- | +* | "Book Cover" | +* +* @param {Book_Detail_Admin_Book_CoverInputs} inputs +* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options +* @returns {LocalizedString} +*/ +export const book_detail_admin_book_cover = /** @type {((inputs?: Book_Detail_Admin_Book_CoverInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata} */ ((inputs = {}, options = {}) => { + const locale = experimentalStaticLocale ?? options.locale ?? getLocale() + if (locale === "en") return en_book_detail_admin_book_cover(inputs) + if (locale === "ru") return ru_book_detail_admin_book_cover(inputs) + if (locale === "id") return id_book_detail_admin_book_cover(inputs) + if (locale === "pt") return pt_book_detail_admin_book_cover(inputs) + return fr_book_detail_admin_book_cover(inputs) +}); \ No newline at end of file diff --git a/ui/src/lib/paraglide/messages/book_detail_admin_cancel_audio.js b/ui/src/lib/paraglide/messages/book_detail_admin_cancel_audio.js new file mode 100644 index 0000000..beeb9e5 --- /dev/null +++ b/ui/src/lib/paraglide/messages/book_detail_admin_cancel_audio.js @@ -0,0 +1,44 @@ +/* eslint-disable */ +import { getLocale, experimentalStaticLocale } from '../runtime.js'; + +/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */ + +/** @typedef {{}} Book_Detail_Admin_Cancel_AudioInputs */ + +const en_book_detail_admin_cancel_audio = /** @type {(inputs: Book_Detail_Admin_Cancel_AudioInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Cancel`) +}; + +const ru_book_detail_admin_cancel_audio = /** @type {(inputs: Book_Detail_Admin_Cancel_AudioInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Отмена`) +}; + +const id_book_detail_admin_cancel_audio = /** @type {(inputs: Book_Detail_Admin_Cancel_AudioInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Batal`) +}; + +const pt_book_detail_admin_cancel_audio = /** @type {(inputs: Book_Detail_Admin_Cancel_AudioInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Cancelar`) +}; + +const fr_book_detail_admin_cancel_audio = /** @type {(inputs: Book_Detail_Admin_Cancel_AudioInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Annuler`) +}; + +/** +* | output | +* | --- | +* | "Cancel" | +* +* @param {Book_Detail_Admin_Cancel_AudioInputs} inputs +* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options +* @returns {LocalizedString} +*/ +export const book_detail_admin_cancel_audio = /** @type {((inputs?: Book_Detail_Admin_Cancel_AudioInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata} */ ((inputs = {}, options = {}) => { + const locale = experimentalStaticLocale ?? options.locale ?? getLocale() + if (locale === "en") return en_book_detail_admin_cancel_audio(inputs) + if (locale === "ru") return ru_book_detail_admin_cancel_audio(inputs) + if (locale === "id") return id_book_detail_admin_cancel_audio(inputs) + if (locale === "pt") return pt_book_detail_admin_cancel_audio(inputs) + return fr_book_detail_admin_cancel_audio(inputs) +}); \ No newline at end of file diff --git a/ui/src/lib/paraglide/messages/book_detail_admin_chapter_cover.js b/ui/src/lib/paraglide/messages/book_detail_admin_chapter_cover.js new file mode 100644 index 0000000..28a59ea --- /dev/null +++ b/ui/src/lib/paraglide/messages/book_detail_admin_chapter_cover.js @@ -0,0 +1,44 @@ +/* eslint-disable */ +import { getLocale, experimentalStaticLocale } from '../runtime.js'; + +/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */ + +/** @typedef {{}} Book_Detail_Admin_Chapter_CoverInputs */ + +const en_book_detail_admin_chapter_cover = /** @type {(inputs: Book_Detail_Admin_Chapter_CoverInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Chapter Cover`) +}; + +const ru_book_detail_admin_chapter_cover = /** @type {(inputs: Book_Detail_Admin_Chapter_CoverInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Обложка главы`) +}; + +const id_book_detail_admin_chapter_cover = /** @type {(inputs: Book_Detail_Admin_Chapter_CoverInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Sampul Bab`) +}; + +const pt_book_detail_admin_chapter_cover = /** @type {(inputs: Book_Detail_Admin_Chapter_CoverInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Capa do Capítulo`) +}; + +const fr_book_detail_admin_chapter_cover = /** @type {(inputs: Book_Detail_Admin_Chapter_CoverInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Couverture du chapitre`) +}; + +/** +* | output | +* | --- | +* | "Chapter Cover" | +* +* @param {Book_Detail_Admin_Chapter_CoverInputs} inputs +* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options +* @returns {LocalizedString} +*/ +export const book_detail_admin_chapter_cover = /** @type {((inputs?: Book_Detail_Admin_Chapter_CoverInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata} */ ((inputs = {}, options = {}) => { + const locale = experimentalStaticLocale ?? options.locale ?? getLocale() + if (locale === "en") return en_book_detail_admin_chapter_cover(inputs) + if (locale === "ru") return ru_book_detail_admin_chapter_cover(inputs) + if (locale === "id") return id_book_detail_admin_chapter_cover(inputs) + if (locale === "pt") return pt_book_detail_admin_chapter_cover(inputs) + return fr_book_detail_admin_chapter_cover(inputs) +}); \ No newline at end of file diff --git a/ui/src/lib/paraglide/messages/book_detail_admin_chapter_n.js b/ui/src/lib/paraglide/messages/book_detail_admin_chapter_n.js new file mode 100644 index 0000000..f48dda2 --- /dev/null +++ b/ui/src/lib/paraglide/messages/book_detail_admin_chapter_n.js @@ -0,0 +1,44 @@ +/* eslint-disable */ +import { getLocale, experimentalStaticLocale } from '../runtime.js'; + +/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */ + +/** @typedef {{}} Book_Detail_Admin_Chapter_NInputs */ + +const en_book_detail_admin_chapter_n = /** @type {(inputs: Book_Detail_Admin_Chapter_NInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Chapter #`) +}; + +const ru_book_detail_admin_chapter_n = /** @type {(inputs: Book_Detail_Admin_Chapter_NInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Глава №`) +}; + +const id_book_detail_admin_chapter_n = /** @type {(inputs: Book_Detail_Admin_Chapter_NInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Bab #`) +}; + +const pt_book_detail_admin_chapter_n = /** @type {(inputs: Book_Detail_Admin_Chapter_NInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Capítulo nº`) +}; + +const fr_book_detail_admin_chapter_n = /** @type {(inputs: Book_Detail_Admin_Chapter_NInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Chapitre n°`) +}; + +/** +* | output | +* | --- | +* | "Chapter #" | +* +* @param {Book_Detail_Admin_Chapter_NInputs} inputs +* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options +* @returns {LocalizedString} +*/ +export const book_detail_admin_chapter_n = /** @type {((inputs?: Book_Detail_Admin_Chapter_NInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata} */ ((inputs = {}, options = {}) => { + const locale = experimentalStaticLocale ?? options.locale ?? getLocale() + if (locale === "en") return en_book_detail_admin_chapter_n(inputs) + if (locale === "ru") return ru_book_detail_admin_chapter_n(inputs) + if (locale === "id") return id_book_detail_admin_chapter_n(inputs) + if (locale === "pt") return pt_book_detail_admin_chapter_n(inputs) + return fr_book_detail_admin_chapter_n(inputs) +}); \ No newline at end of file diff --git a/ui/src/lib/paraglide/messages/book_detail_admin_chapter_names.js b/ui/src/lib/paraglide/messages/book_detail_admin_chapter_names.js new file mode 100644 index 0000000..8ff82a1 --- /dev/null +++ b/ui/src/lib/paraglide/messages/book_detail_admin_chapter_names.js @@ -0,0 +1,44 @@ +/* eslint-disable */ +import { getLocale, experimentalStaticLocale } from '../runtime.js'; + +/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */ + +/** @typedef {{}} Book_Detail_Admin_Chapter_NamesInputs */ + +const en_book_detail_admin_chapter_names = /** @type {(inputs: Book_Detail_Admin_Chapter_NamesInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Chapter Names`) +}; + +const ru_book_detail_admin_chapter_names = /** @type {(inputs: Book_Detail_Admin_Chapter_NamesInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Названия глав`) +}; + +const id_book_detail_admin_chapter_names = /** @type {(inputs: Book_Detail_Admin_Chapter_NamesInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Nama Bab`) +}; + +const pt_book_detail_admin_chapter_names = /** @type {(inputs: Book_Detail_Admin_Chapter_NamesInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Nomes dos Capítulos`) +}; + +const fr_book_detail_admin_chapter_names = /** @type {(inputs: Book_Detail_Admin_Chapter_NamesInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Noms des chapitres`) +}; + +/** +* | output | +* | --- | +* | "Chapter Names" | +* +* @param {Book_Detail_Admin_Chapter_NamesInputs} inputs +* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options +* @returns {LocalizedString} +*/ +export const book_detail_admin_chapter_names = /** @type {((inputs?: Book_Detail_Admin_Chapter_NamesInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata} */ ((inputs = {}, options = {}) => { + const locale = experimentalStaticLocale ?? options.locale ?? getLocale() + if (locale === "en") return en_book_detail_admin_chapter_names(inputs) + if (locale === "ru") return ru_book_detail_admin_chapter_names(inputs) + if (locale === "id") return id_book_detail_admin_chapter_names(inputs) + if (locale === "pt") return pt_book_detail_admin_chapter_names(inputs) + return fr_book_detail_admin_chapter_names(inputs) +}); \ No newline at end of file diff --git a/ui/src/lib/paraglide/messages/book_detail_admin_description.js b/ui/src/lib/paraglide/messages/book_detail_admin_description.js new file mode 100644 index 0000000..eed9c56 --- /dev/null +++ b/ui/src/lib/paraglide/messages/book_detail_admin_description.js @@ -0,0 +1,44 @@ +/* eslint-disable */ +import { getLocale, experimentalStaticLocale } from '../runtime.js'; + +/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */ + +/** @typedef {{}} Book_Detail_Admin_DescriptionInputs */ + +const en_book_detail_admin_description = /** @type {(inputs: Book_Detail_Admin_DescriptionInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Description`) +}; + +const ru_book_detail_admin_description = /** @type {(inputs: Book_Detail_Admin_DescriptionInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Описание`) +}; + +const id_book_detail_admin_description = /** @type {(inputs: Book_Detail_Admin_DescriptionInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Deskripsi`) +}; + +const pt_book_detail_admin_description = /** @type {(inputs: Book_Detail_Admin_DescriptionInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Descrição`) +}; + +const fr_book_detail_admin_description = /** @type {(inputs: Book_Detail_Admin_DescriptionInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Description`) +}; + +/** +* | output | +* | --- | +* | "Description" | +* +* @param {Book_Detail_Admin_DescriptionInputs} inputs +* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options +* @returns {LocalizedString} +*/ +export const book_detail_admin_description = /** @type {((inputs?: Book_Detail_Admin_DescriptionInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata} */ ((inputs = {}, options = {}) => { + const locale = experimentalStaticLocale ?? options.locale ?? getLocale() + if (locale === "en") return en_book_detail_admin_description(inputs) + if (locale === "ru") return ru_book_detail_admin_description(inputs) + if (locale === "id") return id_book_detail_admin_description(inputs) + if (locale === "pt") return pt_book_detail_admin_description(inputs) + return fr_book_detail_admin_description(inputs) +}); \ No newline at end of file diff --git a/ui/src/lib/paraglide/messages/book_detail_admin_discard.js b/ui/src/lib/paraglide/messages/book_detail_admin_discard.js new file mode 100644 index 0000000..5ae703a --- /dev/null +++ b/ui/src/lib/paraglide/messages/book_detail_admin_discard.js @@ -0,0 +1,44 @@ +/* eslint-disable */ +import { getLocale, experimentalStaticLocale } from '../runtime.js'; + +/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */ + +/** @typedef {{}} Book_Detail_Admin_DiscardInputs */ + +const en_book_detail_admin_discard = /** @type {(inputs: Book_Detail_Admin_DiscardInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Discard`) +}; + +const ru_book_detail_admin_discard = /** @type {(inputs: Book_Detail_Admin_DiscardInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Отменить`) +}; + +const id_book_detail_admin_discard = /** @type {(inputs: Book_Detail_Admin_DiscardInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Buang`) +}; + +const pt_book_detail_admin_discard = /** @type {(inputs: Book_Detail_Admin_DiscardInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Descartar`) +}; + +const fr_book_detail_admin_discard = /** @type {(inputs: Book_Detail_Admin_DiscardInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Ignorer`) +}; + +/** +* | output | +* | --- | +* | "Discard" | +* +* @param {Book_Detail_Admin_DiscardInputs} inputs +* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options +* @returns {LocalizedString} +*/ +export const book_detail_admin_discard = /** @type {((inputs?: Book_Detail_Admin_DiscardInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata} */ ((inputs = {}, options = {}) => { + const locale = experimentalStaticLocale ?? options.locale ?? getLocale() + if (locale === "en") return en_book_detail_admin_discard(inputs) + if (locale === "ru") return ru_book_detail_admin_discard(inputs) + if (locale === "id") return id_book_detail_admin_discard(inputs) + if (locale === "pt") return pt_book_detail_admin_discard(inputs) + return fr_book_detail_admin_discard(inputs) +}); \ No newline at end of file diff --git a/ui/src/lib/paraglide/messages/book_detail_admin_enqueue_audio.js b/ui/src/lib/paraglide/messages/book_detail_admin_enqueue_audio.js new file mode 100644 index 0000000..c193315 --- /dev/null +++ b/ui/src/lib/paraglide/messages/book_detail_admin_enqueue_audio.js @@ -0,0 +1,44 @@ +/* eslint-disable */ +import { getLocale, experimentalStaticLocale } from '../runtime.js'; + +/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */ + +/** @typedef {{}} Book_Detail_Admin_Enqueue_AudioInputs */ + +const en_book_detail_admin_enqueue_audio = /** @type {(inputs: Book_Detail_Admin_Enqueue_AudioInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Enqueue Audio`) +}; + +const ru_book_detail_admin_enqueue_audio = /** @type {(inputs: Book_Detail_Admin_Enqueue_AudioInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Поставить в очередь`) +}; + +const id_book_detail_admin_enqueue_audio = /** @type {(inputs: Book_Detail_Admin_Enqueue_AudioInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Antre Audio`) +}; + +const pt_book_detail_admin_enqueue_audio = /** @type {(inputs: Book_Detail_Admin_Enqueue_AudioInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Enfileirar Áudio`) +}; + +const fr_book_detail_admin_enqueue_audio = /** @type {(inputs: Book_Detail_Admin_Enqueue_AudioInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Mettre en file audio`) +}; + +/** +* | output | +* | --- | +* | "Enqueue Audio" | +* +* @param {Book_Detail_Admin_Enqueue_AudioInputs} inputs +* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options +* @returns {LocalizedString} +*/ +export const book_detail_admin_enqueue_audio = /** @type {((inputs?: Book_Detail_Admin_Enqueue_AudioInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata} */ ((inputs = {}, options = {}) => { + const locale = experimentalStaticLocale ?? options.locale ?? getLocale() + if (locale === "en") return en_book_detail_admin_enqueue_audio(inputs) + if (locale === "ru") return ru_book_detail_admin_enqueue_audio(inputs) + if (locale === "id") return id_book_detail_admin_enqueue_audio(inputs) + if (locale === "pt") return pt_book_detail_admin_enqueue_audio(inputs) + return fr_book_detail_admin_enqueue_audio(inputs) +}); \ No newline at end of file diff --git a/ui/src/lib/paraglide/messages/book_detail_admin_enqueued.js b/ui/src/lib/paraglide/messages/book_detail_admin_enqueued.js new file mode 100644 index 0000000..d071edb --- /dev/null +++ b/ui/src/lib/paraglide/messages/book_detail_admin_enqueued.js @@ -0,0 +1,44 @@ +/* eslint-disable */ +import { getLocale, experimentalStaticLocale } from '../runtime.js'; + +/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */ + +/** @typedef {{ enqueued: NonNullable, skipped: NonNullable }} Book_Detail_Admin_EnqueuedInputs */ + +const en_book_detail_admin_enqueued = /** @type {(inputs: Book_Detail_Admin_EnqueuedInputs) => LocalizedString} */ (i) => { + return /** @type {LocalizedString} */ (`Enqueued ${i?.enqueued}, skipped ${i?.skipped}`) +}; + +const ru_book_detail_admin_enqueued = /** @type {(inputs: Book_Detail_Admin_EnqueuedInputs) => LocalizedString} */ (i) => { + return /** @type {LocalizedString} */ (`В очереди ${i?.enqueued}, пропущено ${i?.skipped}`) +}; + +const id_book_detail_admin_enqueued = /** @type {(inputs: Book_Detail_Admin_EnqueuedInputs) => LocalizedString} */ (i) => { + return /** @type {LocalizedString} */ (`Diantre ${i?.enqueued}, dilewati ${i?.skipped}`) +}; + +const pt_book_detail_admin_enqueued = /** @type {(inputs: Book_Detail_Admin_EnqueuedInputs) => LocalizedString} */ (i) => { + return /** @type {LocalizedString} */ (`${i?.enqueued} enfileirados, ${i?.skipped} ignorados`) +}; + +const fr_book_detail_admin_enqueued = /** @type {(inputs: Book_Detail_Admin_EnqueuedInputs) => LocalizedString} */ (i) => { + return /** @type {LocalizedString} */ (`${i?.enqueued} en file, ${i?.skipped} ignorés`) +}; + +/** +* | output | +* | --- | +* | "Enqueued {enqueued}, skipped {skipped}" | +* +* @param {Book_Detail_Admin_EnqueuedInputs} inputs +* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options +* @returns {LocalizedString} +*/ +export const book_detail_admin_enqueued = /** @type {((inputs: Book_Detail_Admin_EnqueuedInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata} */ ((inputs, options = {}) => { + const locale = experimentalStaticLocale ?? options.locale ?? getLocale() + if (locale === "en") return en_book_detail_admin_enqueued(inputs) + if (locale === "ru") return ru_book_detail_admin_enqueued(inputs) + if (locale === "id") return id_book_detail_admin_enqueued(inputs) + if (locale === "pt") return pt_book_detail_admin_enqueued(inputs) + return fr_book_detail_admin_enqueued(inputs) +}); \ No newline at end of file diff --git a/ui/src/lib/paraglide/messages/book_detail_admin_generate.js b/ui/src/lib/paraglide/messages/book_detail_admin_generate.js new file mode 100644 index 0000000..4cf0e1b --- /dev/null +++ b/ui/src/lib/paraglide/messages/book_detail_admin_generate.js @@ -0,0 +1,44 @@ +/* eslint-disable */ +import { getLocale, experimentalStaticLocale } from '../runtime.js'; + +/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */ + +/** @typedef {{}} Book_Detail_Admin_GenerateInputs */ + +const en_book_detail_admin_generate = /** @type {(inputs: Book_Detail_Admin_GenerateInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Generate`) +}; + +const ru_book_detail_admin_generate = /** @type {(inputs: Book_Detail_Admin_GenerateInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Сгенерировать`) +}; + +const id_book_detail_admin_generate = /** @type {(inputs: Book_Detail_Admin_GenerateInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Buat`) +}; + +const pt_book_detail_admin_generate = /** @type {(inputs: Book_Detail_Admin_GenerateInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Gerar`) +}; + +const fr_book_detail_admin_generate = /** @type {(inputs: Book_Detail_Admin_GenerateInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Générer`) +}; + +/** +* | output | +* | --- | +* | "Generate" | +* +* @param {Book_Detail_Admin_GenerateInputs} inputs +* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options +* @returns {LocalizedString} +*/ +export const book_detail_admin_generate = /** @type {((inputs?: Book_Detail_Admin_GenerateInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata} */ ((inputs = {}, options = {}) => { + const locale = experimentalStaticLocale ?? options.locale ?? getLocale() + if (locale === "en") return en_book_detail_admin_generate(inputs) + if (locale === "ru") return ru_book_detail_admin_generate(inputs) + if (locale === "id") return id_book_detail_admin_generate(inputs) + if (locale === "pt") return pt_book_detail_admin_generate(inputs) + return fr_book_detail_admin_generate(inputs) +}); \ No newline at end of file diff --git a/ui/src/lib/paraglide/messages/book_detail_admin_save_cover.js b/ui/src/lib/paraglide/messages/book_detail_admin_save_cover.js new file mode 100644 index 0000000..eabfeb6 --- /dev/null +++ b/ui/src/lib/paraglide/messages/book_detail_admin_save_cover.js @@ -0,0 +1,44 @@ +/* eslint-disable */ +import { getLocale, experimentalStaticLocale } from '../runtime.js'; + +/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */ + +/** @typedef {{}} Book_Detail_Admin_Save_CoverInputs */ + +const en_book_detail_admin_save_cover = /** @type {(inputs: Book_Detail_Admin_Save_CoverInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Save Cover`) +}; + +const ru_book_detail_admin_save_cover = /** @type {(inputs: Book_Detail_Admin_Save_CoverInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Сохранить обложку`) +}; + +const id_book_detail_admin_save_cover = /** @type {(inputs: Book_Detail_Admin_Save_CoverInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Simpan Sampul`) +}; + +const pt_book_detail_admin_save_cover = /** @type {(inputs: Book_Detail_Admin_Save_CoverInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Salvar Capa`) +}; + +const fr_book_detail_admin_save_cover = /** @type {(inputs: Book_Detail_Admin_Save_CoverInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Enregistrer la couverture`) +}; + +/** +* | output | +* | --- | +* | "Save Cover" | +* +* @param {Book_Detail_Admin_Save_CoverInputs} inputs +* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options +* @returns {LocalizedString} +*/ +export const book_detail_admin_save_cover = /** @type {((inputs?: Book_Detail_Admin_Save_CoverInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata} */ ((inputs = {}, options = {}) => { + const locale = experimentalStaticLocale ?? options.locale ?? getLocale() + if (locale === "en") return en_book_detail_admin_save_cover(inputs) + if (locale === "ru") return ru_book_detail_admin_save_cover(inputs) + if (locale === "id") return id_book_detail_admin_save_cover(inputs) + if (locale === "pt") return pt_book_detail_admin_save_cover(inputs) + return fr_book_detail_admin_save_cover(inputs) +}); \ No newline at end of file diff --git a/ui/src/lib/paraglide/messages/book_detail_admin_saved.js b/ui/src/lib/paraglide/messages/book_detail_admin_saved.js new file mode 100644 index 0000000..9034ff1 --- /dev/null +++ b/ui/src/lib/paraglide/messages/book_detail_admin_saved.js @@ -0,0 +1,44 @@ +/* eslint-disable */ +import { getLocale, experimentalStaticLocale } from '../runtime.js'; + +/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */ + +/** @typedef {{}} Book_Detail_Admin_SavedInputs */ + +const en_book_detail_admin_saved = /** @type {(inputs: Book_Detail_Admin_SavedInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Saved`) +}; + +const ru_book_detail_admin_saved = /** @type {(inputs: Book_Detail_Admin_SavedInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Сохранено`) +}; + +const id_book_detail_admin_saved = /** @type {(inputs: Book_Detail_Admin_SavedInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Tersimpan`) +}; + +const pt_book_detail_admin_saved = /** @type {(inputs: Book_Detail_Admin_SavedInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Salvo`) +}; + +const fr_book_detail_admin_saved = /** @type {(inputs: Book_Detail_Admin_SavedInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Enregistré`) +}; + +/** +* | output | +* | --- | +* | "Saved" | +* +* @param {Book_Detail_Admin_SavedInputs} inputs +* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options +* @returns {LocalizedString} +*/ +export const book_detail_admin_saved = /** @type {((inputs?: Book_Detail_Admin_SavedInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata} */ ((inputs = {}, options = {}) => { + const locale = experimentalStaticLocale ?? options.locale ?? getLocale() + if (locale === "en") return en_book_detail_admin_saved(inputs) + if (locale === "ru") return ru_book_detail_admin_saved(inputs) + if (locale === "id") return id_book_detail_admin_saved(inputs) + if (locale === "pt") return pt_book_detail_admin_saved(inputs) + return fr_book_detail_admin_saved(inputs) +}); \ No newline at end of file diff --git a/ui/src/lib/paraglide/messages/book_detail_admin_saving.js b/ui/src/lib/paraglide/messages/book_detail_admin_saving.js new file mode 100644 index 0000000..cecb2db --- /dev/null +++ b/ui/src/lib/paraglide/messages/book_detail_admin_saving.js @@ -0,0 +1,44 @@ +/* eslint-disable */ +import { getLocale, experimentalStaticLocale } from '../runtime.js'; + +/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */ + +/** @typedef {{}} Book_Detail_Admin_SavingInputs */ + +const en_book_detail_admin_saving = /** @type {(inputs: Book_Detail_Admin_SavingInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Saving…`) +}; + +const ru_book_detail_admin_saving = /** @type {(inputs: Book_Detail_Admin_SavingInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Сохранение…`) +}; + +const id_book_detail_admin_saving = /** @type {(inputs: Book_Detail_Admin_SavingInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Menyimpan…`) +}; + +const pt_book_detail_admin_saving = /** @type {(inputs: Book_Detail_Admin_SavingInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Salvando…`) +}; + +const fr_book_detail_admin_saving = /** @type {(inputs: Book_Detail_Admin_SavingInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Enregistrement…`) +}; + +/** +* | output | +* | --- | +* | "Saving…" | +* +* @param {Book_Detail_Admin_SavingInputs} inputs +* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options +* @returns {LocalizedString} +*/ +export const book_detail_admin_saving = /** @type {((inputs?: Book_Detail_Admin_SavingInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata} */ ((inputs = {}, options = {}) => { + const locale = experimentalStaticLocale ?? options.locale ?? getLocale() + if (locale === "en") return en_book_detail_admin_saving(inputs) + if (locale === "ru") return ru_book_detail_admin_saving(inputs) + if (locale === "id") return id_book_detail_admin_saving(inputs) + if (locale === "pt") return pt_book_detail_admin_saving(inputs) + return fr_book_detail_admin_saving(inputs) +}); \ No newline at end of file diff --git a/ui/src/lib/paraglide/messages/book_detail_admin_voice.js b/ui/src/lib/paraglide/messages/book_detail_admin_voice.js new file mode 100644 index 0000000..a5732da --- /dev/null +++ b/ui/src/lib/paraglide/messages/book_detail_admin_voice.js @@ -0,0 +1,44 @@ +/* eslint-disable */ +import { getLocale, experimentalStaticLocale } from '../runtime.js'; + +/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */ + +/** @typedef {{}} Book_Detail_Admin_VoiceInputs */ + +const en_book_detail_admin_voice = /** @type {(inputs: Book_Detail_Admin_VoiceInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Voice`) +}; + +const ru_book_detail_admin_voice = /** @type {(inputs: Book_Detail_Admin_VoiceInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Голос`) +}; + +const id_book_detail_admin_voice = /** @type {(inputs: Book_Detail_Admin_VoiceInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Suara`) +}; + +const pt_book_detail_admin_voice = /** @type {(inputs: Book_Detail_Admin_VoiceInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Voz`) +}; + +const fr_book_detail_admin_voice = /** @type {(inputs: Book_Detail_Admin_VoiceInputs) => LocalizedString} */ () => { + return /** @type {LocalizedString} */ (`Voix`) +}; + +/** +* | output | +* | --- | +* | "Voice" | +* +* @param {Book_Detail_Admin_VoiceInputs} inputs +* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options +* @returns {LocalizedString} +*/ +export const book_detail_admin_voice = /** @type {((inputs?: Book_Detail_Admin_VoiceInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata} */ ((inputs = {}, options = {}) => { + const locale = experimentalStaticLocale ?? options.locale ?? getLocale() + if (locale === "en") return en_book_detail_admin_voice(inputs) + if (locale === "ru") return ru_book_detail_admin_voice(inputs) + if (locale === "id") return id_book_detail_admin_voice(inputs) + if (locale === "pt") return pt_book_detail_admin_voice(inputs) + return fr_book_detail_admin_voice(inputs) +}); \ No newline at end of file diff --git a/ui/src/routes/api/admin/audio/bulk/+server.ts b/ui/src/routes/api/admin/audio/bulk/+server.ts new file mode 100644 index 0000000..29d729f --- /dev/null +++ b/ui/src/routes/api/admin/audio/bulk/+server.ts @@ -0,0 +1,34 @@ +/** + * POST /api/admin/audio/bulk + * + * Admin-only proxy to the Go backend's audio bulk-enqueue endpoint. + * Body: { slug, voice?, from, to, skip_existing?, force? } + * Response 202: { enqueued, skipped, task_ids } + */ + +import { json, error } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { log } from '$lib/server/logger'; +import { backendFetch } from '$lib/server/scraper'; + +export const POST: RequestHandler = async ({ request, locals }) => { + if (!locals.user || locals.user.role !== 'admin') { + throw error(403, 'Forbidden'); + } + + const body = await request.text(); + let res: Response; + try { + res = await backendFetch('/api/admin/audio/bulk', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body + }); + } catch (e) { + log.error('admin/audio/bulk', 'backend proxy error', { err: String(e) }); + throw error(502, 'Could not reach backend'); + } + + const data = await res.json().catch(() => ({})); + return json(data, { status: res.status }); +}; diff --git a/ui/src/routes/api/admin/audio/cancel-bulk/+server.ts b/ui/src/routes/api/admin/audio/cancel-bulk/+server.ts new file mode 100644 index 0000000..cb6b2e0 --- /dev/null +++ b/ui/src/routes/api/admin/audio/cancel-bulk/+server.ts @@ -0,0 +1,34 @@ +/** + * POST /api/admin/audio/cancel-bulk + * + * Admin-only proxy to cancel all pending/running audio tasks for a slug. + * Body: { slug } + * Response 200: { cancelled } + */ + +import { json, error } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { log } from '$lib/server/logger'; +import { backendFetch } from '$lib/server/scraper'; + +export const POST: RequestHandler = async ({ request, locals }) => { + if (!locals.user || locals.user.role !== 'admin') { + throw error(403, 'Forbidden'); + } + + const body = await request.text(); + let res: Response; + try { + res = await backendFetch('/api/admin/audio/cancel-bulk', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body + }); + } catch (e) { + log.error('admin/audio/cancel-bulk', 'backend proxy error', { err: String(e) }); + throw error(502, 'Could not reach backend'); + } + + const data = await res.json().catch(() => ({})); + return json(data, { status: res.status }); +}; diff --git a/ui/src/routes/books/[slug]/+page.svelte b/ui/src/routes/books/[slug]/+page.svelte index 9863a5d..2b19f28 100644 --- a/ui/src/routes/books/[slug]/+page.svelte +++ b/ui/src/routes/books/[slug]/+page.svelte @@ -133,6 +133,301 @@ // ── Admin panel expand/collapse ─────────────────────────────────────────── let adminOpen = $state(false); + // ── Admin: book cover generation ────────────────────────────────────────── + let coverGenerating = $state(false); + let coverPreview = $state(null); + let coverSaving = $state(false); + let coverResult = $state<'saved' | 'error' | ''>(''); + + async function generateCover() { + const slug = data.book?.slug; + if (coverGenerating || !slug) return; + coverGenerating = true; + coverPreview = null; + coverResult = ''; + try { + const res = await fetch('/api/admin/image-gen', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ slug, type: 'cover', prompt: data.book?.title ?? slug }) + }); + if (res.ok) { + const d = await res.json(); + coverPreview = d.image_b64 ? `data:${d.content_type ?? 'image/png'};base64,${d.image_b64}` : null; + } else { + coverResult = 'error'; + } + } catch { + coverResult = 'error'; + } finally { + coverGenerating = false; + } + } + + async function saveCover() { + const slug = data.book?.slug; + if (coverSaving || !coverPreview || !slug) return; + coverSaving = true; + coverResult = ''; + try { + const b64 = coverPreview.replace(/^data:[^;]+;base64,/, ''); + const res = await fetch('/api/admin/image-gen/save-cover', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ slug, image_b64: b64 }) + }); + if (res.ok) { + coverResult = 'saved'; + coverPreview = null; + await invalidateAll(); + } else { + coverResult = 'error'; + } + } catch { + coverResult = 'error'; + } finally { + coverSaving = false; + } + } + + // ── Admin: chapter cover generation ─────────────────────────────────────── + let chapterCoverN = $state('1'); + let chapterCoverGenerating = $state(false); + let chapterCoverPreview = $state(null); + let chapterCoverResult = $state<'error' | ''>(''); + + async function generateChapterCover() { + const slug = data.book?.slug; + if (chapterCoverGenerating || !slug) return; + const n = parseInt(chapterCoverN, 10); + if (!n || n < 1) return; + chapterCoverGenerating = true; + chapterCoverPreview = null; + chapterCoverResult = ''; + try { + const res = await fetch('/api/admin/image-gen', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ slug, type: 'chapter', chapter: n, prompt: data.book?.title ?? slug }) + }); + if (res.ok) { + const d = await res.json(); + chapterCoverPreview = d.image_b64 ? `data:${d.content_type ?? 'image/png'};base64,${d.image_b64}` : null; + } else { + chapterCoverResult = 'error'; + } + } catch { + chapterCoverResult = 'error'; + } finally { + chapterCoverGenerating = false; + } + } + + // ── Admin: description generation ───────────────────────────────────────── + let descGenerating = $state(false); + let descPreview = $state(''); + let descApplying = $state(false); + let descResult = $state<'applied' | 'error' | ''>(''); + + async function generateDesc() { + const slug = data.book?.slug; + if (descGenerating || !slug) return; + descGenerating = true; + descPreview = ''; + descResult = ''; + try { + const res = await fetch('/api/admin/text-gen/description', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ slug }) + }); + if (res.ok) { + const d = await res.json(); + descPreview = d.new_description ?? ''; + } else { + descResult = 'error'; + } + } catch { + descResult = 'error'; + } finally { + descGenerating = false; + } + } + + async function applyDesc() { + const slug = data.book?.slug; + if (descApplying || !descPreview || !slug) return; + descApplying = true; + descResult = ''; + try { + const res = await fetch('/api/admin/text-gen/description/apply', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ slug, description: descPreview }) + }); + if (res.ok) { + descResult = 'applied'; + descPreview = ''; + await invalidateAll(); + } else { + descResult = 'error'; + } + } catch { + descResult = 'error'; + } finally { + descApplying = false; + } + } + + // ── Admin: chapter names generation ─────────────────────────────────────── + let chapNamesGenerating = $state(false); + let chapNamesPreview = $state<{ number: number; old_title: string; new_title: string }[]>([]); + let chapNamesApplying = $state(false); + let chapNamesResult = $state<'applied' | 'error' | ''>(''); + + async function generateChapNames() { + const slug = data.book?.slug; + if (chapNamesGenerating || !slug) return; + chapNamesGenerating = true; + chapNamesPreview = []; + chapNamesResult = ''; + try { + const res = await fetch('/api/admin/text-gen/chapter-names', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ slug, pattern: 'Chapter {n}: {scene}' }) + }); + if (res.ok) { + const d = await res.json(); + chapNamesPreview = d.chapters ?? []; + } else { + chapNamesResult = 'error'; + } + } catch { + chapNamesResult = 'error'; + } finally { + chapNamesGenerating = false; + } + } + + async function applyChapNames() { + const slug = data.book?.slug; + if (chapNamesApplying || chapNamesPreview.length === 0 || !slug) return; + chapNamesApplying = true; + chapNamesResult = ''; + try { + const chapters = chapNamesPreview.map((c) => ({ number: c.number, title: c.new_title })); + const res = await fetch('/api/admin/text-gen/chapter-names/apply', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ slug, chapters }) + }); + if (res.ok) { + chapNamesResult = 'applied'; + chapNamesPreview = []; + await invalidateAll(); + } else { + chapNamesResult = 'error'; + } + } catch { + chapNamesResult = 'error'; + } finally { + chapNamesApplying = false; + } + } + + // ── Admin: audio TTS bulk enqueue ───────────────────────────────────────── + interface Voice { id: string; engine: string; lang: string; gender: string } + let audioVoices = $state([]); + let audioVoicesLoaded = $state(false); + let audioVoice = $state('af_bella'); + let audioFrom = $state('1'); + let audioTo = $state(''); + let audioEnqueuing = $state(false); + let audioResult = $state<{ enqueued: number; skipped: number } | null>(null); + let audioError = $state(''); + + // Load voices lazily when admin panel opens + $effect(() => { + if (!adminOpen || audioVoicesLoaded) return; + fetch('/api/voices') + .then((r) => r.json()) + .then((d: { voices: Voice[] }) => { + audioVoices = d.voices ?? []; + audioVoicesLoaded = true; + }) + .catch(() => { audioVoicesLoaded = true; }); + }); + + function voiceLabel(v: Voice): string { + if (v.engine === 'cfai') { + const speaker = v.id.startsWith('cfai:') ? v.id.slice(5) : v.id; + return speaker.replace(/\b\w/g, (c) => c.toUpperCase()) + ' (CF AI)'; + } + if (v.engine === 'pocket-tts') { + return v.id.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) + ' (Pocket)'; + } + // Kokoro + const langMap: Record = { + af: 'US', am: 'US', bf: 'UK', bm: 'UK', + ef: 'ES', em: 'ES', ff: 'FR', hf: 'IN', hm: 'IN', + 'if': 'IT', im: 'IT', jf: 'JP', jm: 'JP', pf: 'PT', pm: 'PT', zf: 'ZH', zm: 'ZH', + }; + const prefix = v.id.slice(0, 2); + const name = v.id.slice(3).replace(/^v0/, '').replace(/^([a-z])/, (c) => c.toUpperCase()); + const lang = langMap[prefix] ?? prefix.toUpperCase(); + return `${name} (${lang})`; + } + + async function enqueueAudio() { + const slug = data.book?.slug; + if (audioEnqueuing || !slug) return; + const from = parseInt(audioFrom, 10); + const to = audioTo ? parseInt(audioTo, 10) : (data.book?.total_chapters ?? 1); + if (!from || from < 1) return; + audioEnqueuing = true; + audioResult = null; + audioError = ''; + try { + const res = await fetch('/api/admin/audio/bulk', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ slug, voice: audioVoice, from, to }) + }); + if (res.ok || res.status === 202) { + const d = await res.json(); + audioResult = { enqueued: d.enqueued ?? 0, skipped: d.skipped ?? 0 }; + } else { + const d = await res.json().catch(() => ({})); + audioError = d.error ?? 'Failed to enqueue'; + } + } catch { + audioError = 'Network error'; + } finally { + audioEnqueuing = false; + } + } + + async function cancelAudio() { + const slug = data.book?.slug; + if (!slug) return; + audioResult = null; + audioError = ''; + try { + const res = await fetch('/api/admin/audio/cancel-bulk', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ slug }) + }); + if (res.ok) { + const d = await res.json(); + audioError = `Cancelled ${d.cancelled ?? 0} task(s).`; + } + } catch { + audioError = 'Cancel failed'; + } + } + // ── "More like this" ───────────────────────────────────────────────────── interface SimilarBook { slug: string; title: string; cover: string | null; author: string | null } let similarBooks = $state([]); @@ -520,78 +815,336 @@ - {#if adminOpen} -
- + {#if adminOpen} +
+ +
+ + {#if scrapeResult} + + {scrapeResult === 'queued' ? m.catalogue_scrape_queued_badge() + '.' : scrapeResult === 'busy' ? m.catalogue_scrape_busy_badge() + '.' : m.common_error() + '.'} + + {/if} +
+ + +
+
+ + +
+
+ + +
+ + {#if rangeResult} + + {rangeResult === 'queued' ? m.catalogue_scrape_queued_badge() + '.' : rangeResult === 'busy' ? m.catalogue_scrape_busy_badge() + '.' : m.common_error() + '.'} + + {/if} +
+ +
+ + +
+

{m.book_detail_admin_book_cover()}

- {#if scrapeResult} - - {scrapeResult === 'queued' ? m.catalogue_scrape_queued_badge() + '.' : scrapeResult === 'busy' ? m.catalogue_scrape_busy_badge() + '.' : m.common_error() + '.'} - + {#if coverResult === 'error'} + {m.common_error()} + {:else if coverResult === 'saved'} + {m.book_detail_admin_saved()} {/if}
- - -
-
- - + {#if coverPreview} +
+ Cover preview +
+ + +
+ {/if} +
+ + +
+

{m.book_detail_admin_chapter_cover()}

+
- +
- {#if rangeResult} - - {rangeResult === 'queued' ? m.catalogue_scrape_queued_badge() + '.' : rangeResult === 'busy' ? m.catalogue_scrape_busy_badge() + '.' : m.common_error() + '.'} - + {#if chapterCoverResult === 'error'} + {m.common_error()} + {/if} +
+ {#if chapterCoverPreview} +
+ Chapter cover preview + +
+ {/if} +
+ +
+ + +
+

{m.book_detail_admin_description()}

+
+ + {#if descResult === 'error'} + {m.common_error()} + {:else if descResult === 'applied'} + {m.book_detail_admin_applied()} + {/if} +
+ {#if descPreview} +
+ +
+ + +
+
+ {/if} +
+ + +
+

{m.book_detail_admin_chapter_names()}

+
+ + {#if chapNamesResult === 'error'} + {m.common_error()} + {:else if chapNamesResult === 'applied'} + {m.book_detail_admin_applied()} ({chapNamesPreview.length > 0 ? chapNamesPreview.length : ''}) + {/if} +
+ {#if chapNamesPreview.length > 0} +
+ {#each chapNamesPreview as ch} +
+ {ch.number}. + {ch.new_title} +
+ {/each} +
+
+ + +
+ {/if} +
+ +
+ + +
+

{m.book_detail_admin_audio_tts()}

+
+ +
+ + +
+ +
+
+ + +
+
+ + +
+ + +
+ {#if audioResult} + {m.book_detail_admin_enqueued({ enqueued: audioResult.enqueued, skipped: audioResult.skipped })} + {/if} + {#if audioError} + {audioError} {/if}
- {/if} +
+ {/if}
{/if}