From 525ff85a21413eb14a8110fd6f7cc56ed32be384 Mon Sep 17 00:00:00 2001 From: InstaZDLL Date: Fri, 15 May 2026 21:08:39 +0200 Subject: [PATCH 1/2] feat(playlist): add filename sort mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Many users (including the project owner) number their files manually (`01. Track.mp3`, `02. Track.mp3`) and rely on filename ordering to preserve a sequence that doesn't match any tag-derived field. The new mode sorts on the cross-platform basename of `file_path` with `Intl.Collator` (numeric:true) so "1 …" / "2 …" / "10 …" land in natural order like Explorer/Finder. --- CLAUDE.md | 2 +- src/components/views/PlaylistView.tsx | 25 ++++++++++++++++++++++++- src/i18n/locales/ar.json | 3 ++- src/i18n/locales/de.json | 3 ++- src/i18n/locales/en.json | 3 ++- src/i18n/locales/es.json | 3 ++- src/i18n/locales/fr.json | 3 ++- src/i18n/locales/hi.json | 3 ++- src/i18n/locales/id.json | 3 ++- src/i18n/locales/it.json | 3 ++- src/i18n/locales/ja.json | 3 ++- src/i18n/locales/kr.json | 3 ++- src/i18n/locales/nl.json | 3 ++- src/i18n/locales/pt-BR.json | 3 ++- src/i18n/locales/pt.json | 3 ++- src/i18n/locales/ru.json | 3 ++- src/i18n/locales/tr.json | 3 ++- src/i18n/locales/zh-CN.json | 3 ++- src/i18n/locales/zh-TW.json | 3 ++- 19 files changed, 59 insertions(+), 19 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7b70f0a..bb5c7a1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -97,7 +97,7 @@ Player bar Spotify-style right cluster (Mini-player + Fullscreen primary, Speed/ ### Playlists ([`docs/features/playlists.md`](docs/features/playlists.md), [`docs/features/smart-playlists.md`](docs/features/smart-playlists.md)) -Playlist sort dropdown (custom / title / artist / album / recently added / duration — non-custom modes are display-only via `Intl.Collator`, never touch `playlist_track.position`) · auto-cover (Spotify-style 2×2 grid composite from first 4 tracks; manual upload flips `cover_is_auto=0`) · smart playlists (Daily Mix family + recursive boolean rule tree via `CustomRules`, v1 flat → v2 tree auto-migration) · M3U import/export. +Playlist sort dropdown (custom / title / artist / album / recently added / duration / filename — non-custom modes are display-only via `Intl.Collator`, never touch `playlist_track.position`; filename sorts on the cross-platform basename of `track.file_path`) · auto-cover (Spotify-style 2×2 grid composite from first 4 tracks; manual upload flips `cover_is_auto=0`) · smart playlists (Daily Mix family + recursive boolean rule tree via `CustomRules`, v1 flat → v2 tree auto-migration) · M3U import/export. ### Integrations ([`docs/features/integrations.md`](docs/features/integrations.md)) diff --git a/src/components/views/PlaylistView.tsx b/src/components/views/PlaylistView.tsx index 02d8d70..ac05aa6 100644 --- a/src/components/views/PlaylistView.tsx +++ b/src/components/views/PlaylistView.tsx @@ -88,7 +88,8 @@ type PlaylistSortMode = | "artist" | "album" | "added_at" - | "duration_ms"; + | "duration_ms" + | "filename"; const PLAYLIST_SORT_MODES: ReadonlyArray = [ "custom", @@ -97,8 +98,19 @@ const PLAYLIST_SORT_MODES: ReadonlyArray = [ "album", "added_at", "duration_ms", + "filename", ]; +/** Cross-platform basename — handles both Windows (`\`) and POSIX + * (`/`) separators since profiles can ship libraries scanned on + * either OS, and an imported `.waveflow` archive may cross + * platforms. */ +function basename(path: string): string { + const slash = path.lastIndexOf("/"); + const back = path.lastIndexOf("\\"); + return path.slice(Math.max(slash, back) + 1); +} + function isPlaylistSortMode(value: string): value is PlaylistSortMode { return (PLAYLIST_SORT_MODES as readonly string[]).includes(value); } @@ -203,6 +215,16 @@ export function PlaylistView({ case "duration_ms": sorted.sort((a, b) => (b.duration_ms ?? 0) - (a.duration_ms ?? 0)); break; + case "filename": + // Numeric collator gives a natural order on "1 …", "2 …", + // "10 …" filenames — the most common manual-numbering scheme + // (matches Explorer / Finder behaviour). Sorted on the basename + // only so users grouping by parent folder still see filename + // order, not full-path lexicographic order. + sorted.sort((a, b) => + collator.compare(basename(a.file_path), basename(b.file_path)), + ); + break; } return sorted; }, [tracks, sortMode]); @@ -1225,6 +1247,7 @@ function PlaylistSortMenu({ current, onChange, t }: PlaylistSortMenuProps) { album: t("sort.album"), added_at: t("sort.recentlyAdded", "Recently added"), duration_ms: t("sort.duration"), + filename: t("sort.filename", "Filename"), }; return ( diff --git a/src/i18n/locales/ar.json b/src/i18n/locales/ar.json index 73f0a1d..217f807 100644 --- a/src/i18n/locales/ar.json +++ b/src/i18n/locales/ar.json @@ -1036,7 +1036,8 @@ "albumsCount": "عدد الألبومات", "tracksCount": "عدد الأغاني", "ascending": "كروسان", - "descending": "تنازلي" + "descending": "تنازلي", + "filename": "اسم الملف" }, "settings": { "title": "الإعدادات", diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index 1e564c6..d86cb65 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -1036,7 +1036,8 @@ "albumsCount": "Anzahl der Alben", "tracksCount": "Anzahl der Titel", "ascending": "Croissant", - "descending": "Absteigend" + "descending": "Absteigend", + "filename": "Dateiname" }, "settings": { "title": "Einstellungen", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 60ce1c6..1ec14b3 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1048,7 +1048,8 @@ "albumsCount": "Number of albums", "tracksCount": "Number of tracks", "ascending": "Croissant", - "descending": "Descending" + "descending": "Descending", + "filename": "Filename" }, "settings": { "title": "Settings", diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 2c28c72..372a1ee 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -1036,7 +1036,8 @@ "albumsCount": "N.º de álbumes", "tracksCount": "N.º de canciones", "ascending": "Croissant", - "descending": "Descendente" + "descending": "Descendente", + "filename": "Nombre del archivo" }, "settings": { "title": "Configuración", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 713ed69..0c7b0b2 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -1048,7 +1048,8 @@ "albumsCount": "Nb albums", "tracksCount": "Nb morceaux", "ascending": "Croissant", - "descending": "Décroissant" + "descending": "Décroissant", + "filename": "Nom de fichier" }, "settings": { "title": "Paramètres", diff --git a/src/i18n/locales/hi.json b/src/i18n/locales/hi.json index 3b89c6f..210339f 100644 --- a/src/i18n/locales/hi.json +++ b/src/i18n/locales/hi.json @@ -1036,7 +1036,8 @@ "albumsCount": "एल्बमों की संख्या", "tracksCount": "ट्रैकों की संख्या", "ascending": "क्रोइसाँ", - "descending": "उतरता हुआ" + "descending": "उतरता हुआ", + "filename": "फ़ाइल का नाम" }, "settings": { "title": "सेटिंग्स", diff --git a/src/i18n/locales/id.json b/src/i18n/locales/id.json index b8b7c67..56dd0d3 100644 --- a/src/i18n/locales/id.json +++ b/src/i18n/locales/id.json @@ -1036,7 +1036,8 @@ "albumsCount": "Album", "tracksCount": "Lagu", "ascending": "Naik", - "descending": "Menurun" + "descending": "Menurun", + "filename": "Nama berkas" }, "settings": { "title": "Pengaturan", diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json index a8938c9..4d72507 100644 --- a/src/i18n/locales/it.json +++ b/src/i18n/locales/it.json @@ -1036,7 +1036,8 @@ "albumsCount": "Numero di album", "tracksCount": "Numero di brani", "ascending": "Croissant", - "descending": "Decrescente" + "descending": "Decrescente", + "filename": "Nome file" }, "settings": { "title": "Impostazioni", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index a42cd44..72201af 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -1036,7 +1036,8 @@ "albumsCount": "アルバム数", "tracksCount": "曲数", "ascending": "クロワッサン", - "descending": "降順" + "descending": "降順", + "filename": "ファイル名" }, "settings": { "title": "設定", diff --git a/src/i18n/locales/kr.json b/src/i18n/locales/kr.json index 54461de..cc0cce1 100644 --- a/src/i18n/locales/kr.json +++ b/src/i18n/locales/kr.json @@ -1036,7 +1036,8 @@ "albumsCount": "앨범 수", "tracksCount": "곡 수", "ascending": "크루아상", - "descending": "내림차순" + "descending": "내림차순", + "filename": "파일 이름" }, "settings": { "title": "설정", diff --git a/src/i18n/locales/nl.json b/src/i18n/locales/nl.json index 29f623c..0b3dc5e 100644 --- a/src/i18n/locales/nl.json +++ b/src/i18n/locales/nl.json @@ -1036,7 +1036,8 @@ "albumsCount": "Aantal albums", "tracksCount": "Aantal nummers", "ascending": "Croissant", - "descending": "Afnemend" + "descending": "Afnemend", + "filename": "Bestandsnaam" }, "settings": { "title": "Instellingen", diff --git a/src/i18n/locales/pt-BR.json b/src/i18n/locales/pt-BR.json index 7f8e226..e2d0bdc 100644 --- a/src/i18n/locales/pt-BR.json +++ b/src/i18n/locales/pt-BR.json @@ -1036,7 +1036,8 @@ "albumsCount": "Número de álbuns", "tracksCount": "Número de faixas", "ascending": "Croissant", - "descending": "Decrescente" + "descending": "Decrescente", + "filename": "Nome do arquivo" }, "settings": { "title": "Configurações", diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index 0c18f2f..ebbacdf 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -1036,7 +1036,8 @@ "albumsCount": "N.º de álbuns", "tracksCount": "N.º de faixas", "ascending": "Croissant", - "descending": "Decrescente" + "descending": "Decrescente", + "filename": "Nome do ficheiro" }, "settings": { "title": "Parâmetros", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index e94774e..c2ba8f5 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -1036,7 +1036,8 @@ "albumsCount": "Альбомы", "tracksCount": "Треки", "ascending": "По возрастанию", - "descending": "По убыванию" + "descending": "По убыванию", + "filename": "Имя файла" }, "settings": { "title": "Настройки", diff --git a/src/i18n/locales/tr.json b/src/i18n/locales/tr.json index 3af474b..c74dbd7 100644 --- a/src/i18n/locales/tr.json +++ b/src/i18n/locales/tr.json @@ -1036,7 +1036,8 @@ "albumsCount": "Albümler", "tracksCount": "Parçalar", "ascending": "Artan", - "descending": "Azalan" + "descending": "Azalan", + "filename": "Dosya adı" }, "settings": { "title": "Ayarlar", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index b98d847..4416dc7 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -1036,7 +1036,8 @@ "albumsCount": "专辑数量", "tracksCount": "曲目数", "ascending": "牛角包", - "descending": "降序" + "descending": "降序", + "filename": "文件名" }, "settings": { "title": "设置", diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index 025d85f..2b85cc6 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -1036,7 +1036,8 @@ "albumsCount": "專輯數量", "tracksCount": "曲目數", "ascending": "可頌", - "descending": "遞減" + "descending": "遞減", + "filename": "檔案名稱" }, "settings": { "title": "設定", From a2e0467dad28bfa911d46d63a8a7cd505acd72d0 Mon Sep 17 00:00:00 2001 From: InstaZDLL Date: Fri, 15 May 2026 21:20:11 +0200 Subject: [PATCH 2/2] fix(playlist): drop dead i18n fallback for sort.filename The key is now defined in all 17 locales (added in the same commit that introduced the sort mode) so the inline English fallback in t("sort.filename", "Filename") is unreachable. Per the project's locale-completeness contract, drop it. --- src/components/views/PlaylistView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/PlaylistView.tsx b/src/components/views/PlaylistView.tsx index ac05aa6..7169b57 100644 --- a/src/components/views/PlaylistView.tsx +++ b/src/components/views/PlaylistView.tsx @@ -1247,7 +1247,7 @@ function PlaylistSortMenu({ current, onChange, t }: PlaylistSortMenuProps) { album: t("sort.album"), added_at: t("sort.recentlyAdded", "Recently added"), duration_ms: t("sort.duration"), - filename: t("sort.filename", "Filename"), + filename: t("sort.filename"), }; return (