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..7169b57 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"), }; 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": "設定",