Compare commits

..

4 Commits

Author SHA1 Message Date
Kendall Garner da445b815d feat(genre): support sorting by track/album count 2026-06-28 19:39:32 -07:00
Tarulia c875146779 feat: add setting for static window title (#2183) 2026-06-29 01:35:42 +00:00
Kendall Garner 9806d2f553 fix: require all sorts to have default value 2026-06-28 17:36:14 -07:00
Kendall Garner 18a7fd0731 disconnect rpc on discord unmount 2026-06-28 15:31:15 -07:00
15 changed files with 115 additions and 30 deletions
+2
View File
@@ -1175,6 +1175,8 @@
"webAudio": "Use web audio", "webAudio": "Use web audio",
"windowBarStyle_description": "Select the style of the window bar", "windowBarStyle_description": "Select the style of the window bar",
"windowBarStyle": "Window bar style", "windowBarStyle": "Window bar style",
"windowBarTrackinfo": "Track info in Window Title",
"windowBarTrackinfo_description": "Show current track's title and artist, queue position, and Playing/Paused state in the Window's Title.",
"zoom_description": "Sets the zoom percentage for the application", "zoom_description": "Sets the zoom percentage for the application",
"zoom": "Zoom percentage", "zoom": "Zoom percentage",
"queryBuilder": "Query builder", "queryBuilder": "Query builder",
@@ -19,7 +19,6 @@ import {
DeleteInternetRadioStationImageResponse, DeleteInternetRadioStationImageResponse,
DeletePlaylistImageArgs, DeletePlaylistImageArgs,
DeletePlaylistImageResponse, DeletePlaylistImageResponse,
genreListSortMap,
InternalControllerEndpoint, InternalControllerEndpoint,
playlistListSortMap, playlistListSortMap,
PlaylistSongListArgs, PlaylistSongListArgs,
@@ -596,26 +595,7 @@ export const NavidromeController: InternalControllerEndpoint = {
}; };
} }
const res = await ndApiClient(apiClientProps).getGenreList({ return SubsonicController.getGenreList(args);
query: {
_end: query.startIndex + (query.limit || 0),
_order: sortOrderMap.navidrome[query.sortOrder],
_sort: genreListSortMap.navidrome[query.sortBy],
_start: query.startIndex,
library_id: getLibraryId(query.musicFolderId),
name: query.searchTerm,
},
});
if (res.status !== 200) {
throw new Error('Failed to get genre list');
}
return {
items: res.body.data.map((genre) => ndNormalize.genre(genre, apiClientProps.server)),
startIndex: query.startIndex || 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
};
}, },
getImageRequest: SubsonicController.getImageRequest, getImageRequest: SubsonicController.getImageRequest,
getImageUrl: SubsonicController.getImageUrl, getImageUrl: SubsonicController.getImageUrl,
@@ -1090,9 +1090,15 @@ export const SubsonicController: InternalControllerEndpoint = {
} }
switch (query.sortBy) { switch (query.sortBy) {
case GenreListSort.ALBUM_COUNT:
results = orderBy(results, [(v) => v.albumCount], [sortOrder]);
break;
case GenreListSort.NAME: case GenreListSort.NAME:
results = orderBy(results, [(v) => v.value.toLowerCase()], [sortOrder]); results = orderBy(results, [(v) => v.value.toLowerCase()], [sortOrder]);
break; break;
case GenreListSort.SONG_COUNT:
results = orderBy(results, [(v) => v.songCount], [sortOrder]);
break;
default: default:
break; break;
} }
@@ -7,11 +7,14 @@ import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-o
import { FILTER_KEYS } from '/@/renderer/features/shared/utils'; import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
import { setMultipleSearchParams } from '/@/renderer/utils/query-params'; import { setMultipleSearchParams } from '/@/renderer/utils/query-params';
import { runInUrlTransition } from '/@/renderer/utils/url-transition'; import { runInUrlTransition } from '/@/renderer/utils/url-transition';
import { AlbumArtistListSort } from '/@/shared/types/domain-types'; import { AlbumArtistListSort, ArtistListSort } from '/@/shared/types/domain-types';
import { ItemListKey } from '/@/shared/types/types'; import { ItemListKey } from '/@/shared/types/types';
export const useAlbumArtistListFilters = () => { export const useAlbumArtistListFilters = () => {
const { sortBy } = useSortByFilter<AlbumArtistListSort>(null, ItemListKey.ALBUM_ARTIST); const { sortBy } = useSortByFilter<AlbumArtistListSort>(
ArtistListSort.NAME,
ItemListKey.ALBUM_ARTIST,
);
const { sortOrder } = useSortOrderFilter(null, ItemListKey.ALBUM_ARTIST); const { sortOrder } = useSortOrderFilter(null, ItemListKey.ALBUM_ARTIST);
@@ -7,7 +7,7 @@ import { ArtistListSort } from '/@/shared/types/domain-types';
import { ItemListKey } from '/@/shared/types/types'; import { ItemListKey } from '/@/shared/types/types';
export const useArtistListFilters = () => { export const useArtistListFilters = () => {
const { sortBy } = useSortByFilter<ArtistListSort>(null, ItemListKey.ARTIST); const { sortBy } = useSortByFilter<ArtistListSort>(ArtistListSort.NAME, ItemListKey.ARTIST);
const { sortOrder } = useSortOrderFilter(null, ItemListKey.ARTIST); const { sortOrder } = useSortOrderFilter(null, ItemListKey.ARTIST);
@@ -81,6 +81,15 @@ export const useDiscordRpc = () => {
privateModeRef.current = privateMode; privateModeRef.current = privateMode;
}, [privateMode]); }, [privateMode]);
// If the component is unmounted while RPC is enabled, quit RPC
useEffect(() => {
return () => {
if (previousEnabledRef.current) {
discordRpc?.quit();
}
};
}, []);
const setActivity = useCallback( const setActivity = useCallback(
async (current: ActivityState, trigger: ActivityTrigger) => { async (current: ActivityState, trigger: ActivityTrigger) => {
const song = current[0]; const song = current[0];
@@ -6,7 +6,7 @@ import { GenreListSort } from '/@/shared/types/domain-types';
import { ItemListKey } from '/@/shared/types/types'; import { ItemListKey } from '/@/shared/types/types';
export const useGenreListFilters = () => { export const useGenreListFilters = () => {
const { sortBy } = useSortByFilter<GenreListSort>(null, ItemListKey.GENRE); const { sortBy } = useSortByFilter<GenreListSort>(GenreListSort.NAME, ItemListKey.GENRE);
const { sortOrder } = useSortOrderFilter(null, ItemListKey.GENRE); const { sortOrder } = useSortOrderFilter(null, ItemListKey.GENRE);
@@ -11,7 +11,10 @@ import { PlaylistListSort } from '/@/shared/types/domain-types';
import { ItemListKey } from '/@/shared/types/types'; import { ItemListKey } from '/@/shared/types/types';
export const usePlaylistListFilters = () => { export const usePlaylistListFilters = () => {
const sortByFilter = useSortByFilter<PlaylistListSort>(null, ItemListKey.PLAYLIST); const sortByFilter = useSortByFilter<PlaylistListSort>(
PlaylistListSort.NAME,
ItemListKey.PLAYLIST,
);
const sortOrderFilter = useSortOrderFilter(null, ItemListKey.PLAYLIST); const sortOrderFilter = useSortOrderFilter(null, ItemListKey.PLAYLIST);
const { searchTerm, setSearchTerm } = useSearchTermFilter(''); const { searchTerm, setSearchTerm } = useSearchTermFilter('');
@@ -63,6 +63,30 @@ export const WindowSettings = memo(() => {
isHidden: !isElectron(), isHidden: !isElectron(),
title: t('setting.windowBarStyle'), title: t('setting.windowBarStyle'),
}, },
{
control: (
<Switch
aria-label="Toggle track info in Window Bar"
defaultChecked={settings.windowBarTrackinfo}
onChange={(e) => {
if (!e) return;
setSettings({
window: {
windowBarTrackinfo: e.currentTarget.checked,
},
});
}}
/>
),
description: t('setting.windowBarTrackinfo', {
context: 'description',
}),
// tab is hidden entirely right now
// but if it was shown we would want to show this option
// as it also controls the tab title in web
isHidden: false,
title: t('setting.windowBarTrackinfo'),
},
{ {
control: ( control: (
<Switch <Switch
@@ -823,18 +823,38 @@ const GENRE_LIST_FILTERS: Partial<
}, },
], ],
[ServerType.NAVIDROME]: [ [ServerType.NAVIDROME]: [
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.albumCount'),
value: GenreListSort.ALBUM_COUNT,
},
{ {
defaultOrder: SortOrder.ASC, defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name'), name: i18n.t('filter.name'),
value: GenreListSort.NAME, value: GenreListSort.NAME,
}, },
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.songCount'),
value: GenreListSort.SONG_COUNT,
},
], ],
[ServerType.SUBSONIC]: [ [ServerType.SUBSONIC]: [
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.albumCount'),
value: GenreListSort.ALBUM_COUNT,
},
{ {
defaultOrder: SortOrder.ASC, defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name'), name: i18n.t('filter.name'),
value: GenreListSort.NAME, value: GenreListSort.NAME,
}, },
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.albumCount'),
value: GenreListSort.SONG_COUNT,
},
], ],
}; };
@@ -8,7 +8,7 @@ import { parseStringParam, setSearchParam } from '/@/renderer/utils/query-params
import { runInUrlTransition } from '/@/renderer/utils/url-transition'; import { runInUrlTransition } from '/@/renderer/utils/url-transition';
import { ItemListKey } from '/@/shared/types/types'; import { ItemListKey } from '/@/shared/types/types';
export const useSortByFilter = <TSortBy>(defaultValue: null | string, listKey: ItemListKey) => { export const useSortByFilter = <TSortBy>(defaultValue: string, listKey: ItemListKey) => {
const server = useCurrentServer(); const server = useCurrentServer();
const { getFilter, setFilter } = useListFilterPersistence(server.id, listKey); const { getFilter, setFilter } = useListFilterPersistence(server.id, listKey);
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
+14 -1
View File
@@ -14,7 +14,13 @@ import macMin from './assets/min-mac.png';
import styles from './window-bar.module.css'; import styles from './window-bar.module.css';
import { useRadioPlayer } from '/@/renderer/features/radio/hooks/use-radio-player'; import { useRadioPlayer } from '/@/renderer/features/radio/hooks/use-radio-player';
import { useAppStore, usePlayerData, usePlayerStatus, useWindowSettings } from '/@/renderer/store'; import {
useAppStore,
usePlayerData,
usePlayerStatus,
useWindowBarTrackinfo,
useWindowSettings,
} from '/@/renderer/store';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { Platform, PlayerStatus } from '/@/shared/types/types'; import { Platform, PlayerStatus } from '/@/shared/types/types';
@@ -130,6 +136,8 @@ const MacOsControls = ({ controls, title }: WindowBarControlsProps) => {
export const WindowBar = () => { export const WindowBar = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { windowBarStyle } = useWindowSettings(); const { windowBarStyle } = useWindowSettings();
const windowBarTrackinfo = useWindowBarTrackinfo();
const playerStatus = usePlayerStatus(); const playerStatus = usePlayerStatus();
const privateMode = useAppStore((state) => state.privateMode); const privateMode = useAppStore((state) => state.privateMode);
const handleMinimize = () => minimize(); const handleMinimize = () => minimize();
@@ -153,6 +161,10 @@ export const WindowBar = () => {
const title = useMemo(() => { const title = useMemo(() => {
const privateModeString = privateMode ? t('page.windowBar.privateMode') : ''; const privateModeString = privateMode ? t('page.windowBar.privateMode') : '';
if (!windowBarTrackinfo) {
return `Feishin${privateMode ? ` ${privateModeString}` : ''}`;
}
// Show radio information if radio is active // Show radio information if radio is active
if (isRadioActive) { if (isRadioActive) {
const radioStatusString = !isRadioPlaying ? t('page.windowBar.paused') : ''; const radioStatusString = !isRadioPlaying ? t('page.windowBar.paused') : '';
@@ -194,6 +206,7 @@ export const WindowBar = () => {
queueLength, queueLength,
stationName, stationName,
t, t,
windowBarTrackinfo,
]); ]);
useEffect(() => { useEffect(() => {
+5
View File
@@ -678,6 +678,7 @@ const WindowSettingsSchema = z.object({
startMinimized: z.boolean(), startMinimized: z.boolean(),
tray: z.boolean(), tray: z.boolean(),
windowBarStyle: z.nativeEnum(Platform), windowBarStyle: z.nativeEnum(Platform),
windowBarTrackinfo: z.boolean(),
}); });
const QueryValueInputTypeSchema = z.enum([ const QueryValueInputTypeSchema = z.enum([
@@ -2014,6 +2015,7 @@ const initialState: SettingsState = {
startMinimized: false, startMinimized: false,
tray: true, tray: true,
windowBarStyle: platformDefaultWindowBarStyle, windowBarStyle: platformDefaultWindowBarStyle,
windowBarTrackinfo: true,
}, },
}; };
@@ -2560,6 +2562,9 @@ export const useWindowSettings = () => useSettingsStore((state) => state.window,
export const useWindowBarStyle = () => export const useWindowBarStyle = () =>
useSettingsStore((state) => state.window.windowBarStyle, shallow); useSettingsStore((state) => state.window.windowBarStyle, shallow);
export const useWindowBarTrackinfo = () =>
useSettingsStore((state) => state.window.windowBarTrackinfo, shallow);
export const useHotkeySettings = () => useSettingsStore((state) => state.hotkeys, shallow); export const useHotkeySettings = () => useSettingsStore((state) => state.hotkeys, shallow);
export const useHotkeyBindings = () => useSettingsStore((state) => state.hotkeys.bindings, shallow); export const useHotkeyBindings = () => useSettingsStore((state) => state.hotkeys.bindings, shallow);
@@ -27,7 +27,9 @@ export enum NDAlbumListSort {
} }
export enum NDGenreListSort { export enum NDGenreListSort {
ALBUM_COUNT = 'albumCount',
NAME = 'name', NAME = 'name',
SONG_COUNT = 'songCount',
} }
export enum NDPlaylistListSort { export enum NDPlaylistListSort {
@@ -754,6 +756,8 @@ const tag = z.object({
const tagList = z.array(tag); const tagList = z.array(tag);
export enum NDTagListSort { export enum NDTagListSort {
ALBUM_COUNT = 'albumCount',
SONG_COUNT = 'songCount',
TAG_VALUE = 'tagValue', TAG_VALUE = 'tagValue',
} }
+18 -2
View File
@@ -155,7 +155,9 @@ export enum ExternalType {
} }
export enum GenreListSort { export enum GenreListSort {
ALBUM_COUNT = 'albumCount',
NAME = 'name', NAME = 'name',
SONG_COUNT = 'songCount',
} }
export enum ImageType { export enum ImageType {
@@ -166,7 +168,9 @@ export enum ImageType {
} }
export enum TagListSort { export enum TagListSort {
ALBUM_COUNT = 'albumCount',
NAME = 'name', NAME = 'name',
SONG_COUNT = 'songCount',
} }
export type Album = { export type Album = {
@@ -430,19 +434,25 @@ type BaseEndpointArgs = {
type GenreListSortMap = { type GenreListSortMap = {
jellyfin: Record<GenreListSort, JFGenreListSort | undefined>; jellyfin: Record<GenreListSort, JFGenreListSort | undefined>;
navidrome: Record<GenreListSort, NDGenreListSort | undefined>; navidrome: Record<GenreListSort, NDGenreListSort>;
subsonic: Record<UserListSort, undefined>; subsonic: Record<GenreListSort, undefined>;
}; };
export const genreListSortMap: GenreListSortMap = { export const genreListSortMap: GenreListSortMap = {
jellyfin: { jellyfin: {
albumCount: undefined,
name: JFGenreListSort.NAME, name: JFGenreListSort.NAME,
songCount: undefined,
}, },
navidrome: { navidrome: {
albumCount: NDGenreListSort.NAME,
name: NDGenreListSort.NAME, name: NDGenreListSort.NAME,
songCount: NDGenreListSort.NAME,
}, },
subsonic: { subsonic: {
albumCount: undefined,
name: undefined, name: undefined,
songCount: undefined,
}, },
}; };
@@ -454,13 +464,19 @@ type TagListSortMap = {
export const tagListSortMap: TagListSortMap = { export const tagListSortMap: TagListSortMap = {
jellyfin: { jellyfin: {
albumCount: undefined,
name: undefined, name: undefined,
songCount: undefined,
}, },
navidrome: { navidrome: {
albumCount: NDTagListSort.ALBUM_COUNT,
name: NDTagListSort.TAG_VALUE, name: NDTagListSort.TAG_VALUE,
songCount: NDTagListSort.SONG_COUNT,
}, },
subsonic: { subsonic: {
albumCount: undefined,
name: undefined, name: undefined,
songCount: undefined,
}, },
}; };