diff --git a/apps/electron-backend/src/app/database/operations/category.operations.ts b/apps/electron-backend/src/app/database/operations/category.operations.ts index a2cc9b49e..d1263b00c 100644 --- a/apps/electron-backend/src/app/database/operations/category.operations.ts +++ b/apps/electron-backend/src/app/database/operations/category.operations.ts @@ -38,6 +38,10 @@ export async function getCategories( playlistId: string, type: 'live' | 'movies' | 'series' ) { + // Xtream categories are inserted once in provider order and existing + // xtream IDs are preserved, so row id order represents server order. + // If partial category re-inserts are added later, persist a provider + // sort index instead of relying on the insertion id. return db .select() .from(schema.categories) diff --git a/apps/web/src/assets/i18n/ar.json b/apps/web/src/assets/i18n/ar.json index 723daab4f..ebf0adda4 100644 --- a/apps/web/src/assets/i18n/ar.json +++ b/apps/web/src/assets/i18n/ar.json @@ -792,6 +792,8 @@ "SORT_NAME_ASC": "الاسم (أ-ي)", "SORT_NAME_DESC": "الاسم (ي-أ)", "SORT_LABEL": "ترتيب: ", + "SORT_SERVER": "ترتيب الخادم", + "CATEGORY_SORT_ARIA": "ترتيب الفئات", "DASHBOARD": { "CONTINUE_WATCHING": "متابعة المشاهدة", "UNTITLED_SOURCE": "مصدر بدون عنوان", diff --git a/apps/web/src/assets/i18n/ary.json b/apps/web/src/assets/i18n/ary.json index e21c67da6..e3ff4dd52 100644 --- a/apps/web/src/assets/i18n/ary.json +++ b/apps/web/src/assets/i18n/ary.json @@ -792,6 +792,8 @@ "SORT_NAME_ASC": "الاسم أ-ي", "SORT_NAME_DESC": "الاسم ي-أ", "SORT_LABEL": "ترتيب: ", + "SORT_SERVER": "ترتيب السيرفر", + "CATEGORY_SORT_ARIA": "ترتيب التصنيفات", "DASHBOARD": { "CONTINUE_WATCHING": "كمل المشاهدة", "UNTITLED_SOURCE": "مصدر بلا عنوان", diff --git a/apps/web/src/assets/i18n/by.json b/apps/web/src/assets/i18n/by.json index 38a14c918..30fa9963a 100644 --- a/apps/web/src/assets/i18n/by.json +++ b/apps/web/src/assets/i18n/by.json @@ -792,6 +792,8 @@ "SORT_NAME_ASC": "Назва А-Я", "SORT_NAME_DESC": "Назва Я-А", "SORT_LABEL": "Сартаваць: ", + "SORT_SERVER": "Парадак сервера", + "CATEGORY_SORT_ARIA": "Сартаваць катэгорыі", "DASHBOARD": { "CONTINUE_WATCHING": "Працягнуць прагляд", "UNTITLED_SOURCE": "Крыніца без назвы", diff --git a/apps/web/src/assets/i18n/de.json b/apps/web/src/assets/i18n/de.json index 6cbf84c63..71f1b91b9 100644 --- a/apps/web/src/assets/i18n/de.json +++ b/apps/web/src/assets/i18n/de.json @@ -792,6 +792,8 @@ "SORT_NAME_ASC": "Name A-Z", "SORT_NAME_DESC": "Name Z-A", "SORT_LABEL": "Sortierung: ", + "SORT_SERVER": "Serverreihenfolge", + "CATEGORY_SORT_ARIA": "Kategorien sortieren", "DASHBOARD": { "CONTINUE_WATCHING": "Weiter schauen", "UNTITLED_SOURCE": "Unbenannte Quelle", diff --git a/apps/web/src/assets/i18n/el.json b/apps/web/src/assets/i18n/el.json index 169dd5d95..00dfb314f 100644 --- a/apps/web/src/assets/i18n/el.json +++ b/apps/web/src/assets/i18n/el.json @@ -792,6 +792,8 @@ "SORT_NAME_ASC": "Όνομα A-Z", "SORT_NAME_DESC": "Όνομα Z-A", "SORT_LABEL": "Ταξινόμηση: ", + "SORT_SERVER": "Σειρά διακομιστή", + "CATEGORY_SORT_ARIA": "Ταξινόμηση κατηγοριών", "DASHBOARD": { "CONTINUE_WATCHING": "Συνέχιση παρακολούθησης", "UNTITLED_SOURCE": "Πηγή χωρίς τίτλο", diff --git a/apps/web/src/assets/i18n/en.json b/apps/web/src/assets/i18n/en.json index faf73197b..5670dbdbe 100644 --- a/apps/web/src/assets/i18n/en.json +++ b/apps/web/src/assets/i18n/en.json @@ -798,6 +798,8 @@ "SORT_NAME_ASC": "Name A-Z", "SORT_NAME_DESC": "Name Z-A", "SORT_LABEL": "Sort: ", + "SORT_SERVER": "Server sorting", + "CATEGORY_SORT_ARIA": "Sort categories", "DASHBOARD": { "CONTINUE_WATCHING": "Continue Watching", "UNTITLED_SOURCE": "Untitled source", diff --git a/apps/web/src/assets/i18n/es.json b/apps/web/src/assets/i18n/es.json index 7ccfed620..8586fe1e2 100644 --- a/apps/web/src/assets/i18n/es.json +++ b/apps/web/src/assets/i18n/es.json @@ -792,6 +792,8 @@ "SORT_NAME_ASC": "Nombre A-Z", "SORT_NAME_DESC": "Nombre Z-A", "SORT_LABEL": "Ordenar: ", + "SORT_SERVER": "Orden del servidor", + "CATEGORY_SORT_ARIA": "Ordenar categorías", "DASHBOARD": { "CONTINUE_WATCHING": "Continuar viendo", "UNTITLED_SOURCE": "Fuente sin título", diff --git a/apps/web/src/assets/i18n/fr.json b/apps/web/src/assets/i18n/fr.json index 35e451955..a0afad51c 100644 --- a/apps/web/src/assets/i18n/fr.json +++ b/apps/web/src/assets/i18n/fr.json @@ -792,6 +792,8 @@ "SORT_NAME_ASC": "Nom A-Z", "SORT_NAME_DESC": "Nom Z-A", "SORT_LABEL": "Trier : ", + "SORT_SERVER": "Ordre du serveur", + "CATEGORY_SORT_ARIA": "Trier les catégories", "DASHBOARD": { "CONTINUE_WATCHING": "Reprendre la lecture", "UNTITLED_SOURCE": "Source sans titre", diff --git a/apps/web/src/assets/i18n/it.json b/apps/web/src/assets/i18n/it.json index 81b47e2ec..382246bd3 100644 --- a/apps/web/src/assets/i18n/it.json +++ b/apps/web/src/assets/i18n/it.json @@ -792,6 +792,8 @@ "SORT_NAME_ASC": "Nome A-Z", "SORT_NAME_DESC": "Nome Z-A", "SORT_LABEL": "Ordina: ", + "SORT_SERVER": "Ordine del server", + "CATEGORY_SORT_ARIA": "Ordina categorie", "DASHBOARD": { "CONTINUE_WATCHING": "Continua a guardare", "UNTITLED_SOURCE": "Sorgente senza titolo", diff --git a/apps/web/src/assets/i18n/ja.json b/apps/web/src/assets/i18n/ja.json index 653ae142e..3cd55a128 100644 --- a/apps/web/src/assets/i18n/ja.json +++ b/apps/web/src/assets/i18n/ja.json @@ -792,6 +792,8 @@ "SORT_NAME_ASC": "名前 A-Z", "SORT_NAME_DESC": "名前 Z-A", "SORT_LABEL": "並び替え:", + "SORT_SERVER": "サーバー順", + "CATEGORY_SORT_ARIA": "カテゴリを並び替え", "DASHBOARD": { "CONTINUE_WATCHING": "視聴を再開", "UNTITLED_SOURCE": "無題のソース", diff --git a/apps/web/src/assets/i18n/ko.json b/apps/web/src/assets/i18n/ko.json index 73b698b3f..27a42a643 100644 --- a/apps/web/src/assets/i18n/ko.json +++ b/apps/web/src/assets/i18n/ko.json @@ -792,6 +792,8 @@ "SORT_NAME_ASC": "이름 A-Z", "SORT_NAME_DESC": "이름 Z-A", "SORT_LABEL": "정렬: ", + "SORT_SERVER": "서버 순서", + "CATEGORY_SORT_ARIA": "카테고리 정렬", "DASHBOARD": { "CONTINUE_WATCHING": "이어서 시청", "UNTITLED_SOURCE": "제목 없는 소스", diff --git a/apps/web/src/assets/i18n/nl.json b/apps/web/src/assets/i18n/nl.json index e4a4f28e2..baa6d432d 100644 --- a/apps/web/src/assets/i18n/nl.json +++ b/apps/web/src/assets/i18n/nl.json @@ -792,6 +792,8 @@ "SORT_NAME_ASC": "Naam A-Z", "SORT_NAME_DESC": "Naam Z-A", "SORT_LABEL": "Sorteren: ", + "SORT_SERVER": "Servervolgorde", + "CATEGORY_SORT_ARIA": "Categorieën sorteren", "DASHBOARD": { "CONTINUE_WATCHING": "Verder kijken", "UNTITLED_SOURCE": "Naamloze bron", diff --git a/apps/web/src/assets/i18n/pl.json b/apps/web/src/assets/i18n/pl.json index c0293fa21..78d897df3 100644 --- a/apps/web/src/assets/i18n/pl.json +++ b/apps/web/src/assets/i18n/pl.json @@ -792,6 +792,8 @@ "SORT_NAME_ASC": "Nazwa A-Z", "SORT_NAME_DESC": "Nazwa Z-A", "SORT_LABEL": "Sortuj: ", + "SORT_SERVER": "Kolejność serwera", + "CATEGORY_SORT_ARIA": "Sortuj kategorie", "DASHBOARD": { "CONTINUE_WATCHING": "Kontynuuj oglądanie", "UNTITLED_SOURCE": "Źródło bez tytułu", diff --git a/apps/web/src/assets/i18n/pt.json b/apps/web/src/assets/i18n/pt.json index 6cada2740..2d9f91a0f 100644 --- a/apps/web/src/assets/i18n/pt.json +++ b/apps/web/src/assets/i18n/pt.json @@ -792,6 +792,8 @@ "SORT_NAME_ASC": "Nome A-Z", "SORT_NAME_DESC": "Nome Z-A", "SORT_LABEL": "Ordenar: ", + "SORT_SERVER": "Ordem do servidor", + "CATEGORY_SORT_ARIA": "Ordenar categorias", "DASHBOARD": { "CONTINUE_WATCHING": "Continuar assistindo", "UNTITLED_SOURCE": "Fonte sem título", diff --git a/apps/web/src/assets/i18n/ru.json b/apps/web/src/assets/i18n/ru.json index af1480a04..f840edb02 100644 --- a/apps/web/src/assets/i18n/ru.json +++ b/apps/web/src/assets/i18n/ru.json @@ -796,6 +796,8 @@ "SORT_NAME_ASC": "Имя (А-Я)", "SORT_NAME_DESC": "Имя (Я-А)", "SORT_LABEL": "Сортировка: ", + "SORT_SERVER": "Порядок сервера", + "CATEGORY_SORT_ARIA": "Сортировать категории", "DASHBOARD": { "CONTINUE_WATCHING": "Продолжить просмотр", "UNTITLED_SOURCE": "Источник без названия", diff --git a/apps/web/src/assets/i18n/tr.json b/apps/web/src/assets/i18n/tr.json index 059d257f1..20a1f36f1 100644 --- a/apps/web/src/assets/i18n/tr.json +++ b/apps/web/src/assets/i18n/tr.json @@ -792,6 +792,8 @@ "SORT_NAME_ASC": "İsim A-Z", "SORT_NAME_DESC": "İsim Z-A", "SORT_LABEL": "Sırala: ", + "SORT_SERVER": "Sunucu sıralaması", + "CATEGORY_SORT_ARIA": "Kategorileri sırala", "DASHBOARD": { "CONTINUE_WATCHING": "İzlemeye Devam Et", "UNTITLED_SOURCE": "Adsız kaynak", diff --git a/apps/web/src/assets/i18n/zh.json b/apps/web/src/assets/i18n/zh.json index a2096f120..02da1e7a5 100644 --- a/apps/web/src/assets/i18n/zh.json +++ b/apps/web/src/assets/i18n/zh.json @@ -792,6 +792,8 @@ "SORT_NAME_ASC": "名称 A-Z", "SORT_NAME_DESC": "名称 Z-A", "SORT_LABEL": "排序:", + "SORT_SERVER": "服务器排序", + "CATEGORY_SORT_ARIA": "排序分类", "DASHBOARD": { "CONTINUE_WATCHING": "继续观看", "UNTITLED_SOURCE": "未命名源", diff --git a/apps/web/src/assets/i18n/zhtw.json b/apps/web/src/assets/i18n/zhtw.json index 747260097..9115b80c1 100644 --- a/apps/web/src/assets/i18n/zhtw.json +++ b/apps/web/src/assets/i18n/zhtw.json @@ -792,6 +792,8 @@ "SORT_NAME_ASC": "名稱 A-Z", "SORT_NAME_DESC": "名稱 Z-A", "SORT_LABEL": "排序:", + "SORT_SERVER": "伺服器排序", + "CATEGORY_SORT_ARIA": "排序分類", "DASHBOARD": { "CONTINUE_WATCHING": "繼續觀看", "UNTITLED_SOURCE": "未命名的來源", diff --git a/libs/portal/shared/util/src/lib/category-sort.spec.ts b/libs/portal/shared/util/src/lib/category-sort.spec.ts index 8b0deda16..dbdebfdf8 100644 --- a/libs/portal/shared/util/src/lib/category-sort.spec.ts +++ b/libs/portal/shared/util/src/lib/category-sort.spec.ts @@ -1,7 +1,6 @@ import { DEFAULT_PORTAL_CATEGORY_SORT_MODE, WORKSPACE_CATEGORY_SORT_STORAGE_KEY, - getPortalCategorySortModeLabel, isPortalCategorySortMode, persistPortalCategorySortMode, restorePortalCategorySortMode, @@ -31,15 +30,11 @@ describe('portal category sort', () => { expect(restorePortalCategorySortMode()).toBe('name-desc'); }); - it('recognizes valid modes and labels them for the category menu', () => { + it('recognizes valid category sort modes', () => { expect(isPortalCategorySortMode('server')).toBe(true); expect(isPortalCategorySortMode('name-asc')).toBe(true); expect(isPortalCategorySortMode('name-desc')).toBe(true); expect(isPortalCategorySortMode('date-desc')).toBe(false); - - expect(getPortalCategorySortModeLabel('server')).toBe('Server sorting'); - expect(getPortalCategorySortModeLabel('name-asc')).toBe('A-Z'); - expect(getPortalCategorySortModeLabel('name-desc')).toBe('Z-A'); }); it('preserves input order for server sorting and supports A-Z/Z-A sorting', () => { diff --git a/libs/portal/shared/util/src/lib/category-sort.ts b/libs/portal/shared/util/src/lib/category-sort.ts index e5a2ec9f8..d32adc144 100644 --- a/libs/portal/shared/util/src/lib/category-sort.ts +++ b/libs/portal/shared/util/src/lib/category-sort.ts @@ -32,20 +32,6 @@ export function persistPortalCategorySortMode( localStorage.setItem(storageKey, mode); } -export function getPortalCategorySortModeLabel( - mode: PortalCategorySortMode -): string { - if (mode === 'name-asc') { - return 'A-Z'; - } - - if (mode === 'name-desc') { - return 'Z-A'; - } - - return 'Server sorting'; -} - export function sortPortalCategoryItems( items: readonly T[], mode: PortalCategorySortMode, diff --git a/libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-context-panel.component.html b/libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-context-panel.component.html index ca038c241..c812c82d5 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-context-panel.component.html +++ b/libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-context-panel.component.html @@ -27,10 +27,15 @@ categorySortMode() !== 'server' " [matMenuTriggerFor]="categorySortMenu" - [matTooltip]="'Sort: ' + categorySortLabel()" - aria-label="Sort categories" + [matTooltip]=" + ('WORKSPACE.SORT_LABEL' | translate) + + (categorySortLabelKey() | translate) + " + [attr.aria-label]=" + 'WORKSPACE.CATEGORY_SORT_ARIA' | translate + " > - sort_by_alpha + {{ categorySortIcon() }} @for ( @@ -42,7 +47,9 @@ (click)="setCategorySortMode(option.mode)" > {{ option.icon }} - {{ option.label }} + {{ + option.translationKey | translate + }} @if (categorySortMode() === option.mode) { check = { 'WORKSPACE.CONTEXT.RADIO_CATEGORIES': 'Radio Categories', 'WORKSPACE.CONTEXT.XTREAM_SYNCING_LIVE': 'Syncing live categories...', 'WORKSPACE.CONTEXT.XTREAM_SYNCING_MOVIES': 'Syncing movie categories...', + 'WORKSPACE.CATEGORY_SORT_ARIA': 'Sort categories', + 'WORKSPACE.SORT_LABEL': 'Sort: ', + 'WORKSPACE.SORT_NAME_ASC': 'Name A-Z', + 'WORKSPACE.SORT_NAME_DESC': 'Name Z-A', + 'WORKSPACE.SORT_SERVER': 'Server sorting', 'WORKSPACE.SHELL.XTREAM_IMPORT_LOADING': 'Fetching playlist data from source...', }; @@ -302,6 +307,55 @@ describe('WorkspaceContextPanelComponent', () => { ); }); + it('uses translated category sort labels and distinct mode icons', () => { + fixture.componentRef.setInput('section', 'vod'); + xtreamSelectedTypeContentState.set('ready'); + fixture.detectChanges(); + + const sortButton = Array.from( + fixture.nativeElement.querySelectorAll('.context-header__action') + ).at(1) as HTMLButtonElement | undefined; + + expect(sortButton?.getAttribute('aria-label')).toBe('Sort categories'); + expect(fixture.componentInstance.categorySortLabelKey()).toBe( + 'WORKSPACE.SORT_SERVER' + ); + expect(fixture.componentInstance.categorySortIcon()).toBe('dns'); + expect( + fixture.componentInstance.categorySortOptions.map((option) => ({ + mode: option.mode, + translationKey: option.translationKey, + icon: option.icon, + })) + ).toEqual([ + { + mode: 'server', + translationKey: 'WORKSPACE.SORT_SERVER', + icon: 'dns', + }, + { + mode: 'name-asc', + translationKey: 'WORKSPACE.SORT_NAME_ASC', + icon: 'sort_by_alpha', + }, + { + mode: 'name-desc', + translationKey: 'WORKSPACE.SORT_NAME_DESC', + icon: 'arrow_downward', + }, + ]); + + fixture.componentInstance.setCategorySortMode('name-desc'); + fixture.detectChanges(); + + expect(fixture.componentInstance.categorySortLabelKey()).toBe( + 'WORKSPACE.SORT_NAME_DESC' + ); + expect(fixture.componentInstance.categorySortIcon()).toBe( + 'arrow_downward' + ); + }); + it('applies the category sort menu modes to stalker categories', () => { fixture.componentRef.setInput('context', { provider: 'stalker', diff --git a/libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-context-panel.component.ts b/libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-context-panel.component.ts index 959a23765..0ff997510 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-context-panel.component.ts +++ b/libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-context-panel.component.ts @@ -20,7 +20,6 @@ import { TranslatePipe, TranslateService } from '@ngx-translate/core'; import { StalkerStore } from '@iptvnator/portal/stalker/data-access'; import { PortalCategorySortMode, - getPortalCategorySortModeLabel, persistPortalCategorySortMode, restorePortalCategorySortMode, sortPortalCategoryItems, @@ -157,30 +156,36 @@ export class WorkspaceContextPanelComponent { readonly categorySortMode = signal( restorePortalCategorySortMode() ); - readonly categorySortLabel = computed(() => - getPortalCategorySortModeLabel(this.categorySortMode()) + readonly categorySortLabelKey = computed(() => + this.getCategorySortLabelKey(this.categorySortMode()) ); readonly categorySortOptions: ReadonlyArray<{ mode: PortalCategorySortMode; - label: string; + translationKey: string; icon: string; }> = [ { mode: 'server', - label: 'Server sorting', + translationKey: 'WORKSPACE.SORT_SERVER', icon: 'dns', }, { mode: 'name-asc', - label: 'A-Z', + translationKey: 'WORKSPACE.SORT_NAME_ASC', icon: 'sort_by_alpha', }, { mode: 'name-desc', - label: 'Z-A', - icon: 'sort_by_alpha', + translationKey: 'WORKSPACE.SORT_NAME_DESC', + icon: 'arrow_downward', }, ]; + readonly categorySortIcon = computed( + () => + this.categorySortOptions.find( + (option) => option.mode === this.categorySortMode() + )?.icon ?? 'dns' + ); readonly canSearchCategories = computed( () => @@ -450,6 +455,18 @@ export class WorkspaceContextPanelComponent { } } + private getCategorySortLabelKey(mode: PortalCategorySortMode): string { + switch (mode) { + case 'name-asc': + return 'WORKSPACE.SORT_NAME_ASC'; + case 'name-desc': + return 'WORKSPACE.SORT_NAME_DESC'; + case 'server': + default: + return 'WORKSPACE.SORT_SERVER'; + } + } + private getCategoryLabel(category: WorkspaceCategoryLike): string { return category.category_name ?? category.name ?? ''; }