From 1bbeda59b68ac1b5f4541c77aacc55e26fce38e7 Mon Sep 17 00:00:00 2001 From: nubiz Date: Wed, 29 Apr 2026 16:18:40 +0200 Subject: [PATCH 1/4] add i18n entries with default to english --- src/i18n/locales/de.json | 9 +++++++-- src/i18n/locales/en.json | 9 +++++++-- src/i18n/locales/es.json | 9 +++++++-- src/i18n/locales/fr.json | 7 ++++++- src/i18n/locales/id.json | 9 +++++++-- src/i18n/locales/it.json | 9 +++++++-- src/i18n/locales/ja.json | 9 +++++++-- src/i18n/locales/ko.json | 9 +++++++-- src/i18n/locales/ms.json | 9 +++++++-- src/i18n/locales/nl.json | 9 +++++++-- src/i18n/locales/ru.json | 9 +++++++-- src/i18n/locales/uk.json | 9 +++++++-- src/i18n/locales/zh-Hans.json | 9 +++++++-- src/i18n/locales/zh-Hant.json | 9 +++++++-- 14 files changed, 97 insertions(+), 27 deletions(-) diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index be208141..9f4717dd 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -910,5 +910,10 @@ "storageNoLimit": "Kein Limit", "storageLegendImages": "Bilder", "storageLegendMusic": "Musik", - "storageLegendFree": "Verfügbar" -} + "storageLegendFree": "Verfügbar", + "showSongInPlaylistOption": "Show if song is in playlist", + "showSongInPlaylist": "Show", + "hideSongInPlaylist": "Hide", + "numberOfTimesInPlaylist": "Times present : {{count}}", + "failedToLoadPlaylistsEntries": "Failed To Load Playlists Entries" +} \ No newline at end of file diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 5b7fe42f..fac2821d 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -929,5 +929,10 @@ "storageNoLimit": "No limit", "storageLegendImages": "Images", "storageLegendMusic": "Music", - "storageLegendFree": "Free" -} + "storageLegendFree": "Free", + "showSongInPlaylistOption": "Show if song is in playlist", + "showSongInPlaylist": "Show", + "hideSongInPlaylist": "Hide", + "numberOfTimesInPlaylist": "Times present : {{count}}", + "failedToLoadPlaylistsEntries": "Failed To Load Playlists Entries" +} \ No newline at end of file diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index ed57361f..aa38effe 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -910,5 +910,10 @@ "storageNoLimit": "Sin límite", "storageLegendImages": "Imágenes", "storageLegendMusic": "Música", - "storageLegendFree": "Disponible" -} + "storageLegendFree": "Disponible", + "showSongInPlaylistOption": "Show if song is in playlist", + "showSongInPlaylist": "Show", + "hideSongInPlaylist": "Hide", + "numberOfTimesInPlaylist": "Times present : {{count}}", + "failedToLoadPlaylistsEntries": "Failed To Load Playlists Entries" +} \ No newline at end of file diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 50574896..5c3fe641 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -910,5 +910,10 @@ "storageNoLimit": "Sans limite", "storageLegendImages": "Images", "storageLegendMusic": "Musique", - "storageLegendFree": "Disponible" + "storageLegendFree": "Disponible", + "showSongInPlaylistOption" : "Afficher si une musique est dans une playlist", + "showSongInPlaylist": "Afficher", + "hideSongInPlaylist": "Cacher", + "numberOfTimesInPlaylist": "Présente {{count}} fois", + "failedToLoadPlaylistsEntries": "Impossible de charger les musiques des playlists" } diff --git a/src/i18n/locales/id.json b/src/i18n/locales/id.json index c548b6cf..22b68524 100644 --- a/src/i18n/locales/id.json +++ b/src/i18n/locales/id.json @@ -910,5 +910,10 @@ "storageNoLimit": "No limit", "storageLegendImages": "Images", "storageLegendMusic": "Music", - "storageLegendFree": "Free" -} + "storageLegendFree": "Free", + "showSongInPlaylistOption": "Show if song is in playlist", + "showSongInPlaylist": "Show", + "hideSongInPlaylist": "Hide", + "numberOfTimesInPlaylist": "Times present : {{count}}", + "failedToLoadPlaylistsEntries": "Failed To Load Playlists Entries" +} \ No newline at end of file diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json index 613abeb8..3b9921d3 100644 --- a/src/i18n/locales/it.json +++ b/src/i18n/locales/it.json @@ -910,5 +910,10 @@ "storageNoLimit": "Nessun limite", "storageLegendImages": "Immagini", "storageLegendMusic": "Musica", - "storageLegendFree": "Disponibile" -} + "storageLegendFree": "Disponibile", + "showSongInPlaylistOption": "Show if song is in playlist", + "showSongInPlaylist": "Show", + "hideSongInPlaylist": "Hide", + "numberOfTimesInPlaylist": "Times present : {{count}}", + "failedToLoadPlaylistsEntries": "Failed To Load Playlists Entries" +} \ No newline at end of file diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index c548b6cf..22b68524 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -910,5 +910,10 @@ "storageNoLimit": "No limit", "storageLegendImages": "Images", "storageLegendMusic": "Music", - "storageLegendFree": "Free" -} + "storageLegendFree": "Free", + "showSongInPlaylistOption": "Show if song is in playlist", + "showSongInPlaylist": "Show", + "hideSongInPlaylist": "Hide", + "numberOfTimesInPlaylist": "Times present : {{count}}", + "failedToLoadPlaylistsEntries": "Failed To Load Playlists Entries" +} \ No newline at end of file diff --git a/src/i18n/locales/ko.json b/src/i18n/locales/ko.json index c548b6cf..22b68524 100644 --- a/src/i18n/locales/ko.json +++ b/src/i18n/locales/ko.json @@ -910,5 +910,10 @@ "storageNoLimit": "No limit", "storageLegendImages": "Images", "storageLegendMusic": "Music", - "storageLegendFree": "Free" -} + "storageLegendFree": "Free", + "showSongInPlaylistOption": "Show if song is in playlist", + "showSongInPlaylist": "Show", + "hideSongInPlaylist": "Hide", + "numberOfTimesInPlaylist": "Times present : {{count}}", + "failedToLoadPlaylistsEntries": "Failed To Load Playlists Entries" +} \ No newline at end of file diff --git a/src/i18n/locales/ms.json b/src/i18n/locales/ms.json index c548b6cf..22b68524 100644 --- a/src/i18n/locales/ms.json +++ b/src/i18n/locales/ms.json @@ -910,5 +910,10 @@ "storageNoLimit": "No limit", "storageLegendImages": "Images", "storageLegendMusic": "Music", - "storageLegendFree": "Free" -} + "storageLegendFree": "Free", + "showSongInPlaylistOption": "Show if song is in playlist", + "showSongInPlaylist": "Show", + "hideSongInPlaylist": "Hide", + "numberOfTimesInPlaylist": "Times present : {{count}}", + "failedToLoadPlaylistsEntries": "Failed To Load Playlists Entries" +} \ No newline at end of file diff --git a/src/i18n/locales/nl.json b/src/i18n/locales/nl.json index c548b6cf..22b68524 100644 --- a/src/i18n/locales/nl.json +++ b/src/i18n/locales/nl.json @@ -910,5 +910,10 @@ "storageNoLimit": "No limit", "storageLegendImages": "Images", "storageLegendMusic": "Music", - "storageLegendFree": "Free" -} + "storageLegendFree": "Free", + "showSongInPlaylistOption": "Show if song is in playlist", + "showSongInPlaylist": "Show", + "hideSongInPlaylist": "Hide", + "numberOfTimesInPlaylist": "Times present : {{count}}", + "failedToLoadPlaylistsEntries": "Failed To Load Playlists Entries" +} \ No newline at end of file diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index fcf411d6..2957c52c 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -910,5 +910,10 @@ "storageNoLimit": "Без ограничений", "storageLegendImages": "Изображения", "storageLegendMusic": "Музыка", - "storageLegendFree": "Свободно" -} + "storageLegendFree": "Свободно", + "showSongInPlaylistOption": "Show if song is in playlist", + "showSongInPlaylist": "Show", + "hideSongInPlaylist": "Hide", + "numberOfTimesInPlaylist": "Times present : {{count}}", + "failedToLoadPlaylistsEntries": "Failed To Load Playlists Entries" +} \ No newline at end of file diff --git a/src/i18n/locales/uk.json b/src/i18n/locales/uk.json index c548b6cf..22b68524 100644 --- a/src/i18n/locales/uk.json +++ b/src/i18n/locales/uk.json @@ -910,5 +910,10 @@ "storageNoLimit": "No limit", "storageLegendImages": "Images", "storageLegendMusic": "Music", - "storageLegendFree": "Free" -} + "storageLegendFree": "Free", + "showSongInPlaylistOption": "Show if song is in playlist", + "showSongInPlaylist": "Show", + "hideSongInPlaylist": "Hide", + "numberOfTimesInPlaylist": "Times present : {{count}}", + "failedToLoadPlaylistsEntries": "Failed To Load Playlists Entries" +} \ No newline at end of file diff --git a/src/i18n/locales/zh-Hans.json b/src/i18n/locales/zh-Hans.json index 42e9b060..c539518a 100644 --- a/src/i18n/locales/zh-Hans.json +++ b/src/i18n/locales/zh-Hans.json @@ -910,5 +910,10 @@ "storageNoLimit": "无限制", "storageLegendImages": "图像", "storageLegendMusic": "音乐", - "storageLegendFree": "剩余" -} + "storageLegendFree": "剩余", + "showSongInPlaylistOption": "Show if song is in playlist", + "showSongInPlaylist": "Show", + "hideSongInPlaylist": "Hide", + "numberOfTimesInPlaylist": "Times present : {{count}}", + "failedToLoadPlaylistsEntries": "Failed To Load Playlists Entries" +} \ No newline at end of file diff --git a/src/i18n/locales/zh-Hant.json b/src/i18n/locales/zh-Hant.json index 10c8f14d..1baf8dda 100644 --- a/src/i18n/locales/zh-Hant.json +++ b/src/i18n/locales/zh-Hant.json @@ -910,5 +910,10 @@ "storageNoLimit": "無限制", "storageLegendImages": "圖像", "storageLegendMusic": "音樂", - "storageLegendFree": "剩餘" -} + "storageLegendFree": "剩餘", + "showSongInPlaylistOption": "Show if song is in playlist", + "showSongInPlaylist": "Show", + "hideSongInPlaylist": "Hide", + "numberOfTimesInPlaylist": "Times present : {{count}}", + "failedToLoadPlaylistsEntries": "Failed To Load Playlists Entries" +} \ No newline at end of file From ebd268ea78af022a90da58a729e73acb97ae325b Mon Sep 17 00:00:00 2001 From: nubiz Date: Wed, 29 Apr 2026 16:21:12 +0200 Subject: [PATCH 2/4] add setting to show song in playlists --- src/screens/settings-appearance.tsx | 63 +++++++++++++++++++++++++++++ src/store/layoutPreferencesStore.ts | 5 +++ 2 files changed, 68 insertions(+) diff --git a/src/screens/settings-appearance.tsx b/src/screens/settings-appearance.tsx index eb041b01..af28c629 100644 --- a/src/screens/settings-appearance.tsx +++ b/src/screens/settings-appearance.tsx @@ -13,6 +13,7 @@ import type { ThemePreference } from '../store/themeStore'; import { DEFAULT_PRIMARY_COLOR } from '../store/themeStore'; import { layoutPreferencesStore, + ShowSongInPlaylist, type AlbumSortOrder, type ArtistAlbumSortOrder, type DateFormat, @@ -47,6 +48,8 @@ const ALBUM_SORT_OPTIONS: { value: AlbumSortOrder; labelKey: string }[] = [ { value: 'title', labelKey: 'sortAlbumTitle' }, ]; + + const ARTIST_ALBUM_SORT_OPTIONS: { value: ArtistAlbumSortOrder; labelKey: string }[] = [ { value: 'newest', labelKey: 'sortNewestFirst' }, { value: 'oldest', labelKey: 'sortOldestFirst' }, @@ -75,6 +78,10 @@ const ACCENT_COLORS: { labelKey: string; hex: string }[] = [ { labelKey: 'colorYellow', hex: '#FFD600' }, ]; +const SHOW_SONG_IN_PLAYLIST_OPTIONS: { value: ShowSongInPlaylist; labelKey: string }[] = [ + { value: 'show', labelKey: 'showSongInPlaylist' }, + { value: 'hide', labelKey: 'hideSongInPlaylist' }, +]; export function SettingsAppearanceScreen() { const { t } = useTranslation(); const { colors, preference, primaryColor, setThemePreference, setPrimaryColor } = useTheme(); @@ -85,9 +92,12 @@ export function SettingsAppearanceScreen() { const [artistAlbumSortOpen, setArtistAlbumSortOpen] = useState(false); const [dateFormatOpen, setDateFormatOpen] = useState(false); const [listLengthOpen, setListLengthOpen] = useState(false); + const [showSongInPlaylistOpen, setShowSongInPlaylistOpen] = useState(false); + const activeAccentMatch = ACCENT_COLORS.find((c) => c.hex === activePrimary); const activeAccentLabel = activeAccentMatch ? t(activeAccentMatch.labelKey) : t('custom'); + const handleAccentSelect = useCallback( (hex: string) => { setPrimaryColor(hex === DEFAULT_PRIMARY_COLOR ? null : hex); @@ -178,6 +188,9 @@ export function SettingsAppearanceScreen() { favArtistLayout: setFavArtistLayout, }; + const showSongInPlaylist = layoutPreferencesStore((s) => s.showSongInPlaylist); + const setShowSongInPlaylist = layoutPreferencesStore((s) => s.setShowSongInPlaylist); + const dynamicStyles = useMemo( () => StyleSheet.create({ @@ -614,6 +627,56 @@ export function SettingsAppearanceScreen() { + + {t('showSongInPlaylistOption')} + + setShowSongInPlaylistOpen((prev) => !prev)} + style={({ pressed }) => [ + styles.accentHeader, + pressed && settingsStyles.pressed, + ]} + > + + {t(SHOW_SONG_IN_PLAYLIST_OPTIONS.find((o) => o.value === showSongInPlaylist)!.labelKey)}{' '} + + + + {showSongInPlaylistOpen && ( + + {SHOW_SONG_IN_PLAYLIST_OPTIONS.map((opt) => { + const isActive = showSongInPlaylist === opt.value; + return ( + { + setShowSongInPlaylist(opt.value); + setShowSongInPlaylistOpen(false); + }} + style={({ pressed }) => [ + styles.accentOption, + { borderBottomColor: colors.border }, + pressed && settingsStyles.pressed, + ]} + > + + {t(opt.labelKey)}{' '} + + {isActive && ( + + )} + + ); + })} + + )} + + + diff --git a/src/store/layoutPreferencesStore.ts b/src/store/layoutPreferencesStore.ts index ed44cde3..fb671dc8 100644 --- a/src/store/layoutPreferencesStore.ts +++ b/src/store/layoutPreferencesStore.ts @@ -8,6 +8,7 @@ export type AlbumSortOrder = 'artist' | 'title'; export type ArtistAlbumSortOrder = 'newest' | 'oldest'; export type DateFormat = 'yyyy/mm/dd' | 'yyyy/dd/mm'; export type ListLength = 20 | 30 | 50 | 100; +export type ShowSongInPlaylist = 'show' | 'hide'; export const LIST_LENGTH_DISPLAY_CAP = 20; @@ -23,6 +24,7 @@ export interface LayoutPreferencesState { dateFormat: DateFormat; listLength: ListLength; includePartialInDownloadedFilter: boolean; + showSongInPlaylist: ShowSongInPlaylist; setAlbumLayout: (layout: ItemLayout) => void; setArtistLayout: (layout: ItemLayout) => void; setPlaylistLayout: (layout: ItemLayout) => void; @@ -34,6 +36,7 @@ export interface LayoutPreferencesState { setDateFormat: (format: DateFormat) => void; setListLength: (length: ListLength) => void; setIncludePartialInDownloadedFilter: (value: boolean) => void; + setShowSongInPlaylist: (option: ShowSongInPlaylist) => void; } const PERSIST_KEY = 'substreamer-layout-preferences'; @@ -52,6 +55,7 @@ export const layoutPreferencesStore = create()( dateFormat: 'yyyy/mm/dd', listLength: 20, includePartialInDownloadedFilter: false, + showSongInPlaylist: 'hide', setAlbumLayout: (albumLayout) => set({ albumLayout }), setArtistLayout: (artistLayout) => set({ artistLayout }), setPlaylistLayout: (playlistLayout) => set({ playlistLayout }), @@ -65,6 +69,7 @@ export const layoutPreferencesStore = create()( setListLength: (listLength) => set({ listLength }), setIncludePartialInDownloadedFilter: (includePartialInDownloadedFilter) => set({ includePartialInDownloadedFilter }), + setShowSongInPlaylist: (showSongInPlaylist) => set({ showSongInPlaylist }), }), { name: PERSIST_KEY, From d2180ab7ce539d9cf4c92e11539593f11843db90 Mon Sep 17 00:00:00 2001 From: nubiz Date: Wed, 29 Apr 2026 16:22:53 +0200 Subject: [PATCH 3/4] add UI and logic to show if song is in Playlist --- src/components/AddToPlaylistSheet.tsx | 271 +++++++++++++++----------- src/store/playlistLibraryStore.ts | 39 +++- 2 files changed, 193 insertions(+), 117 deletions(-) diff --git a/src/components/AddToPlaylistSheet.tsx b/src/components/AddToPlaylistSheet.tsx index e64cfd7f..c8e195dd 100644 --- a/src/components/AddToPlaylistSheet.tsx +++ b/src/components/AddToPlaylistSheet.tsx @@ -18,6 +18,8 @@ import Animated, { Easing, } from 'react-native-reanimated'; import { useTranslation } from 'react-i18next'; +import i18n from '../i18n/i18n'; + import { BottomSheet } from './BottomSheet'; import { CachedImage } from './CachedImage'; @@ -36,8 +38,9 @@ import { musicCacheStore } from '../store/musicCacheStore'; import { playlistDetailStore } from '../store/playlistDetailStore'; import { playlistLibraryStore } from '../store/playlistLibraryStore'; import { processingOverlayStore } from '../store/processingOverlayStore'; +import { layoutPreferencesStore } from '@/store/layoutPreferencesStore'; -import type { Playlist } from '../services/subsonicService'; +import type { Playlist, PlaylistWithSongs } from '../services/subsonicService'; const CONTENT_DELAY_MS = 750; const CONTENT_ANIMATE_DURATION = 1000; @@ -86,6 +89,7 @@ export function AddToPlaylistSheet() { const playlists = playlistLibraryStore((s) => s.playlists); const playlistsLoading = playlistLibraryStore((s) => s.loading); const playlistsFetchError = playlistLibraryStore((s) => s.error); + const showSongInPlaylist = layoutPreferencesStore((s) => s.showSongInPlaylist); const { colors } = useTheme(); const { t } = useTranslation(); @@ -111,7 +115,11 @@ export function AddToPlaylistSheet() { animatedHeight.value = SPINNER_HEIGHT; contentOpacity.value = 0; const timer = setTimeout(() => { - playlistLibraryStore.getState().fetchAllPlaylists(); + if (showSongInPlaylist === "show") { + playlistLibraryStore.getState().fetchAllPlaylistsWithSongs(); + } else { + playlistLibraryStore.getState().fetchAllPlaylists(); + } setPhase('measuring'); }, CONTENT_DELAY_MS); return () => clearTimeout(timer); @@ -184,8 +192,11 @@ export function AddToPlaylistSheet() { syncCachedItemTracks(playlist.id, updated.entry ?? []); } } - - playlistLibraryStore.getState().fetchAllPlaylists(); + if (showSongInPlaylist === "show") { + playlistLibraryStore.getState().fetchAllPlaylistsWithSongs(); + } else { + playlistLibraryStore.getState().fetchAllPlaylists(); + } processingOverlayStore.getState().showSuccess(t('addedToPlaylist')); } catch { processingOverlayStore.getState().showError(t('failedToAddToPlaylist')); @@ -212,7 +223,11 @@ export function AddToPlaylistSheet() { if (!success) throw new Error('API returned false'); handleClose(); - playlistLibraryStore.getState().fetchAllPlaylists(); + if (showSongInPlaylist === "show") { + playlistLibraryStore.getState().fetchAllPlaylistsWithSongs(); + } else { + playlistLibraryStore.getState().fetchAllPlaylists(); + } processingOverlayStore.getState().show(t('creating')); processingOverlayStore.getState().showSuccess(t('playlistCreated')); } catch { @@ -255,6 +270,22 @@ export function AddToPlaylistSheet() { const subtitle = target ? getSubtitleText(target, t) : ''; const coverArtId = target ? getTargetCoverArt(target) : undefined; + const numberOfMatch = (playlist: Playlist | PlaylistWithSongs) => { + if (!playlist) return 0; + if (!(playlist as PlaylistWithSongs)?.entry) return 0; + + const playlistWithSongs = playlist as PlaylistWithSongs; + + if (target?.type === 'song') { + const matchCount = playlistWithSongs.entry?.filter((current_entry) => target.item.id === current_entry.id).length || 0; + return matchCount; + } + else if (target?.type === "album") { + return 0; + } + + return 0; + } return ( @@ -285,120 +316,129 @@ export function AddToPlaylistSheet() { ]} onLayout={phase === 'measuring' ? handleContentLayout : undefined} > - {mode === 'pick' ? ( - - {/* New Playlist row */} - [ - styles.playlistRow, - pressed && styles.rowPressed, - ]} - > - - - {t('newPlaylist')} - - - - - - {playlists.map((playlist) => ( - handleSelectPlaylist(playlist)} - disabled={busy} - style={({ pressed }) => [ - styles.playlistRow, - pressed && styles.rowPressed, - ]} + {mode === 'pick' ? ( + - - - - {playlist.name} + {/* New Playlist row */} + [ + styles.playlistRow, + pressed && styles.rowPressed, + ]} + > + + + {t('newPlaylist')} - - {t('trackWithCount', { count: playlist.songCount ?? 0 })} + + + + + {playlists.map((playlist) => ( + handleSelectPlaylist(playlist)} + disabled={busy} + style={({ pressed }) => [ + styles.playlistRow, + pressed && styles.rowPressed, + ]} + > + + + + {playlist.name} + + + {t('trackWithCount', { count: playlist.songCount ?? 0 })} + + + {numberOfMatch(playlist) > 0 ? + + + + {i18n.t('numberOfTimesInPlaylist', { count: numberOfMatch(playlist) })} + + + : null} + + ))} + + {playlists.length === 0 && playlistsLoading && ( + + )} + + {playlists.length === 0 && !playlistsLoading && !playlistsFetchError && ( + + {t('noPlaylistsYet')} - - - ))} - - {playlists.length === 0 && playlistsLoading && ( - - )} - - {playlists.length === 0 && !playlistsLoading && !playlistsFetchError && ( - - {t('noPlaylistsYet')} - - )} - - {playlistsFetchError && playlists.length === 0 && !playlistsLoading && ( - - {t('failedToLoadPlaylists')} - - )} + )} - {playlists.length > 0 && playlistsLoading && ( - - )} - - ) : ( - - {/* Back arrow */} - - - {t('back')} - - - {t('playlistName')} - - - {error && ( - {error} + {playlistsFetchError && playlists.length === 0 && !playlistsLoading && ( + + {t('failedToLoadPlaylists')} + + )} + + {playlists.length > 0 && playlistsLoading && ( + + )} + + ) : ( + + {/* Back arrow */} + + + {t('back')} + + + {t('playlistName')} + + + {error && ( + {error} + )} + + [ + styles.createButton, + dynamicStyles.createButton, + pressed && styles.buttonPressed, + busy && styles.buttonDisabled, + ]} + > + {busy ? ( + + ) : ( + <> + + {t('createPlaylist')} + + )} + + )} - - [ - styles.createButton, - dynamicStyles.createButton, - pressed && styles.buttonPressed, - busy && styles.buttonDisabled, - ]} - > - {busy ? ( - - ) : ( - <> - - {t('createPlaylist')} - - )} - - - )} )} @@ -457,8 +497,7 @@ const styles = StyleSheet.create({ minWidth: 0, }, playlistName: { - fontSize: 16, - fontWeight: '500', + fontSize: 14, }, playlistCount: { fontSize: 14, diff --git a/src/store/playlistLibraryStore.ts b/src/store/playlistLibraryStore.ts index e040d971..3796c036 100644 --- a/src/store/playlistLibraryStore.ts +++ b/src/store/playlistLibraryStore.ts @@ -8,6 +8,8 @@ import { kvStorage } from './persistence'; import { ensureCoverArtAuth, getAllPlaylists, + getPlaylist, + PlaylistWithSongs, type Playlist, } from '../services/subsonicService'; @@ -26,7 +28,7 @@ export function registerPlaylistLibraryReconcileHook( export interface PlaylistLibraryState { /** All playlists in the user's library */ - playlists: Playlist[]; + playlists: (Playlist | PlaylistWithSongs)[]; /** Whether a fetch is currently in progress */ loading: boolean; /** Last error message, if any */ @@ -36,6 +38,10 @@ export interface PlaylistLibraryState { /** Fetch all playlists from the server via getPlaylists. */ fetchAllPlaylists: () => Promise; + + /** Fetch all playlists with their songs from the server via getPlaylists and getPlaylist. */ + fetchAllPlaylistsWithSongs: () => Promise; + /** Remove a single playlist from the library by ID. */ removePlaylist: (id: string) => void; /** Clear all playlist data */ @@ -49,6 +55,7 @@ export const playlistLibraryStore = create()( (set, get) => ({ playlists: [], loading: false, + entriesLoading: false, error: null, lastFetchedAt: null, @@ -83,6 +90,36 @@ export const playlistLibraryStore = create()( } }, + fetchAllPlaylistsWithSongs: async () => { + await get().fetchAllPlaylists(); + + if (get().loading) return; + + set({ loading: true, error: null }); + + try { + + const playlistPromises = get().playlists.map(async (playlist) => { + const playlistWithEntries = await getPlaylist(playlist.id); + return playlistWithEntries || playlist; + }); + + const newPlaylists = await Promise.all(playlistPromises); + + set({ + loading: false, + playlists: newPlaylists, + }); + + } catch (e) { + set({ + loading: false, + error: e instanceof Error ? e.message : i18n.t('failedToLoadPlaylistsEntries'), + }); + } + + }, + removePlaylist: (id) => set((state) => ({ playlists: state.playlists.filter((p) => p.id !== id), From be9006072fa1745242d06c2e60570eb1b243e1ff Mon Sep 17 00:00:00 2001 From: nubiz Date: Wed, 29 Apr 2026 17:07:04 +0200 Subject: [PATCH 4/4] add test for store fetchAllPlaylistsWithSongs store logic --- .../__tests__/playlistLibraryStore.test.ts | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/store/__tests__/playlistLibraryStore.test.ts b/src/store/__tests__/playlistLibraryStore.test.ts index 7a899aff..e5ccd1bf 100644 --- a/src/store/__tests__/playlistLibraryStore.test.ts +++ b/src/store/__tests__/playlistLibraryStore.test.ts @@ -1,10 +1,12 @@ jest.mock('../persistence/kvStorage', () => require('../persistence/__mocks__/kvStorage')); jest.mock('../../services/subsonicService'); -import { ensureCoverArtAuth, getAllPlaylists } from '../../services/subsonicService'; +import { ensureCoverArtAuth, getAllPlaylists, getPlaylist, PlaylistWithSongs } from '../../services/subsonicService'; import { playlistLibraryStore } from '../playlistLibraryStore'; const mockGetAllPlaylists = getAllPlaylists as jest.MockedFunction; +const mockGetPlaylist = getPlaylist as jest.MockedFunction; + beforeEach(() => { jest.clearAllMocks(); @@ -12,6 +14,8 @@ beforeEach(() => { }); const makePlaylist = (id: string, name: string) => ({ id, name } as any); +const makePlaylistWithSongs = (id: string, name: string, entry: { id: string }[]) => ({ id, name, entry } as any); + describe('playlistLibraryStore', () => { describe('fetchAllPlaylists', () => { @@ -27,6 +31,27 @@ describe('playlistLibraryStore', () => { expect(state.lastFetchedAt).toBeGreaterThan(0); }); + it('fetches and stores playlists with entries', async () => { + mockGetAllPlaylists.mockResolvedValue([makePlaylist('p1', 'Chill')]); + mockGetPlaylist.mockResolvedValue(makePlaylistWithSongs('p1', 'Chill', [{ id: 's1' }, { id: 's2' }])); + + await playlistLibraryStore.getState().fetchAllPlaylistsWithSongs(); + + expect(ensureCoverArtAuth).toHaveBeenCalled(); + const state = playlistLibraryStore.getState(); + expect(state.playlists).toHaveLength(1); + expect(state.loading).toBe(false); + expect(state.lastFetchedAt).toBeGreaterThan(0); + + const playlistWithSongs = state.playlists[0] as PlaylistWithSongs; + + expect(playlistWithSongs).toBeDefined(); + expect(playlistWithSongs.entry).toBeDefined(); + if (playlistWithSongs.entry) + expect(playlistWithSongs.entry.length).toBe(2) + + }); + it('prevents duplicate fetches', async () => { playlistLibraryStore.setState({ loading: true }); await playlistLibraryStore.getState().fetchAllPlaylists();