feat(settings): add read-aloud filename token

Expose media-overlay-aware naming across file creation and delivery paths while preserving existing client contracts. Keep settings router mocks compatible with the application router exports.

Closes #1590
This commit is contained in:
neon
2026-09-29 01:29:25 +00:00
parent acce1e15ac
commit d66f853cad
28 changed files with 543 additions and 41 deletions
@@ -147,6 +147,18 @@ describe('FileNamingSettings', () => {
}
})
it('offers the readaloud token and previews read-aloud and plain EPUB outcomes', async () => {
const wrapper = await mountPage()
expect(buttonWith(wrapper, '{readaloud}')).toBeDefined()
await patternField(wrapper).setValue('{title}< ({readaloud})>')
expect(wrapper.text()).toContain('Read-aloud comparison')
expect(wrapper.text()).toContain('Read-aloud EPUB')
expect(wrapper.text()).toContain('Plain EPUB')
expect(wrapper.text()).toContain('Neuromancer (readaloud)')
})
it('saves the cross-platform toggle immediately without a separate save button', async () => {
const wrapper = await mountPage()
@@ -14,10 +14,14 @@ const routeState = { name: 'settings-appearance-theme' }
const push = vi.fn<(to: { name: string }) => void>()
vi.mock('vue-router', () => ({
useRoute: () => ({ name: routeState.name }),
useRouter: () => ({ push }),
}))
vi.mock('vue-router', async (importOriginal) => {
const actual = await importOriginal<typeof import('vue-router')>()
return {
...actual,
useRoute: () => ({ name: routeState.name }),
useRouter: () => ({ push }),
}
})
const navStatus = vi.hoisted(() => ({ scanning: false }))
@@ -10,10 +10,14 @@ const permState = {
demoRestricted: false,
}
vi.mock('vue-router', () => ({
useRoute: () => ({ name: 'settings-appearance-theme' }),
useRouter: () => ({ push: vi.fn<(to: { name: string }) => void>() }),
}))
vi.mock('vue-router', async (importOriginal) => {
const actual = await importOriginal<typeof import('vue-router')>()
return {
...actual,
useRoute: () => ({ name: 'settings-appearance-theme' }),
useRouter: () => ({ push: vi.fn<(to: { name: string }) => void>() }),
}
})
vi.mock('@/features/auth/composables/usePermissions', () => ({
usePermissions: () => ({
@@ -36,6 +36,8 @@ function resolve(metadata: Record<string, string>): string {
}
const resolved = computed(() => resolve(EXAMPLE_PATTERN_METADATA))
const usesReadaloudToken = computed(() => props.pattern.includes('{readaloud'))
const plainEpubResult = computed(() => resolve({ ...EXAMPLE_PATTERN_METADATA, readaloud: '' }))
const fallbackCases = computed(() =>
MISSING_METADATA_CASES.map((previewCase) => ({
@@ -113,6 +115,22 @@ function handleSanitize(value: boolean) {
</dl>
</div>
<div v-if="usesReadaloudToken" class="border-t border-border px-3.5 py-3">
<h4 class="mb-1 text-[10px] font-bold uppercase tracking-wider text-muted-foreground">
{{ t('settings.reader.fileNaming.readaloudComparison') }}
</h4>
<dl class="divide-y divide-dashed divide-border">
<div class="py-1.5">
<dt class="text-[11px] font-semibold text-muted-foreground">{{ t('settings.reader.fileNaming.readaloudEpub') }}</dt>
<dd class="mt-0.5 break-words font-mono text-[11px] text-foreground">{{ resolved }}</dd>
</div>
<div class="py-1.5">
<dt class="text-[11px] font-semibold text-muted-foreground">{{ t('settings.reader.fileNaming.plainEpub') }}</dt>
<dd class="mt-0.5 break-words font-mono text-[11px] text-foreground">{{ plainEpubResult }}</dd>
</div>
</dl>
</div>
<div class="flex items-center gap-3 border-t border-border bg-muted/30 px-3.5 py-3">
<div class="min-w-0 flex-1">
<p class="settings-label">{{ t('settings.reader.fileNaming.crossPlatform') }}</p>
@@ -24,6 +24,7 @@ const TOKEN_DESCRIPTION_KEYS: Record<PatternToken, string> = {
library: 'settings.reader.fileNaming.tokenLibrary',
originalFilename: 'settings.reader.fileNaming.tokenOriginalFilename',
extension: 'settings.reader.fileNaming.tokenExtension',
readaloud: 'settings.reader.fileNaming.tokenReadaloud',
}
const MODIFIER_DESCRIPTION_KEYS: Record<PatternModifier, string> = {
+4
View File
@@ -2324,6 +2324,7 @@
"tokenLibrary": "Library name",
"tokenOriginalFilename": "Original filename (without extension)",
"tokenExtension": "File extension (without dot)",
"tokenReadaloud": "The word readaloud for an EPUB with media overlays",
"patternHelpIntro": "Patterns use tokens like {titleToken} and {authorsToken}, plus modifiers and optional segments.",
"browseTokensAndExamples": "Browse tokens and examples",
"subtitleShort": "Folder and filename patterns for stored files.",
@@ -2386,6 +2387,9 @@
"resultCopyFailed": "Failed to copy the resolved result",
"sampleBookNote": "Sample book used for every preview",
"missingMetadata": "If metadata is missing",
"readaloudComparison": "Read-aloud comparison",
"readaloudEpub": "Read-aloud EPUB",
"plainEpub": "Plain EPUB",
"caseNoSeriesLabel": "No series",
"caseNoYearLabel": "No year",
"caseNoAuthorLabel": "No author",