mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-14 20:40:21 +02:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 011f260e94 | |||
| e937425f4f | |||
| bc2624bffd | |||
| 4f21c26e5d | |||
| e6a4ce2e64 | |||
| 5b98238b3a | |||
| d96c0d547a | |||
| 3c62de8347 | |||
| 07d4dc37b5 | |||
| 64c5f25d18 | |||
| 098e86b1f4 | |||
| adc3e421f6 |
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"productName": "Feishin",
|
"productName": "Feishin",
|
||||||
"description": "Feishin music server",
|
"description": "Feishin music server",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\"",
|
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\"",
|
||||||
"build:main": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.prod.ts",
|
"build:main": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.prod.ts",
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "./dist/main/main.js",
|
"main": "./dist/main/main.js",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -87,8 +87,6 @@ ipcMain.on('player-set-queue', async (_event, data: PlayerData, pause?: boolean)
|
|||||||
|
|
||||||
if (pause) {
|
if (pause) {
|
||||||
await getMpvInstance()?.pause();
|
await getMpvInstance()?.pause();
|
||||||
} else {
|
|
||||||
await getMpvInstance()?.play();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -273,7 +273,7 @@ const createWindow = async () => {
|
|||||||
|
|
||||||
const globalMediaKeysEnabled = store.get('global_media_hotkeys') as boolean;
|
const globalMediaKeysEnabled = store.get('global_media_hotkeys') as boolean;
|
||||||
|
|
||||||
if (globalMediaKeysEnabled) {
|
if (globalMediaKeysEnabled !== false) {
|
||||||
enableMediaKeys(mainWindow);
|
enableMediaKeys(mainWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,16 +6,18 @@ import qs from 'qs';
|
|||||||
import { toast } from '/@/renderer/components';
|
import { toast } from '/@/renderer/components';
|
||||||
import { ServerListItem } from '/@/renderer/types';
|
import { ServerListItem } from '/@/renderer/types';
|
||||||
import omitBy from 'lodash/omitBy';
|
import omitBy from 'lodash/omitBy';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
const c = initContract();
|
const c = initContract();
|
||||||
|
|
||||||
export const contract = c.router({
|
export const contract = c.router({
|
||||||
addToPlaylist: {
|
addToPlaylist: {
|
||||||
body: jfType._parameters.addToPlaylist,
|
body: z.null(),
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: 'playlists/:id/items',
|
path: 'playlists/:id/items',
|
||||||
|
query: jfType._parameters.addToPlaylist,
|
||||||
responses: {
|
responses: {
|
||||||
200: jfType._response.addToPlaylist,
|
204: jfType._response.addToPlaylist,
|
||||||
400: jfType._response.error,
|
400: jfType._response.error,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -190,7 +192,7 @@ export const contract = c.router({
|
|||||||
removeFromPlaylist: {
|
removeFromPlaylist: {
|
||||||
body: null,
|
body: null,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
path: 'items/:id',
|
path: 'playlists/:id/items',
|
||||||
query: jfType._parameters.removeFromPlaylist,
|
query: jfType._parameters.removeFromPlaylist,
|
||||||
responses: {
|
responses: {
|
||||||
200: jfType._response.removeFromPlaylist,
|
200: jfType._response.removeFromPlaylist,
|
||||||
|
|||||||
@@ -403,16 +403,17 @@ const addToPlaylist = async (args: AddToPlaylistArgs): Promise<AddToPlaylistResp
|
|||||||
}
|
}
|
||||||
|
|
||||||
const res = await jfApiClient(apiClientProps).addToPlaylist({
|
const res = await jfApiClient(apiClientProps).addToPlaylist({
|
||||||
body: {
|
body: null,
|
||||||
Ids: body.songId,
|
|
||||||
UserId: apiClientProps?.server?.userId,
|
|
||||||
},
|
|
||||||
params: {
|
params: {
|
||||||
id: query.id,
|
id: query.id,
|
||||||
},
|
},
|
||||||
|
query: {
|
||||||
|
Ids: body.songId,
|
||||||
|
UserId: apiClientProps.server?.userId,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 204) {
|
||||||
throw new Error('Failed to add to playlist');
|
throw new Error('Failed to add to playlist');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,7 +435,7 @@ const removeFromPlaylist = async (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 204) {
|
||||||
throw new Error('Failed to remove from playlist');
|
throw new Error('Failed to remove from playlist');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -431,7 +431,7 @@ const removeFromPlaylist = async (
|
|||||||
id: query.id,
|
id: query.id,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
ids: query.songId,
|
id: query.songId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ const removeFromPlaylist = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const removeFromPlaylistParameters = z.object({
|
const removeFromPlaylistParameters = z.object({
|
||||||
ids: z.array(z.string()),
|
id: z.array(z.string()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ndType = {
|
export const ndType = {
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||||||
const handleDeletePlaylist = useCallback(() => {
|
const handleDeletePlaylist = useCallback(() => {
|
||||||
for (const item of ctx.data) {
|
for (const item of ctx.data) {
|
||||||
deletePlaylistMutation?.mutate(
|
deletePlaylistMutation?.mutate(
|
||||||
{ query: { id: item.id } },
|
{ query: { id: item.id }, serverId: item.serverId },
|
||||||
{
|
{
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
toast.error({
|
toast.error({
|
||||||
@@ -432,6 +432,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||||||
id: ctx.context.playlistId,
|
id: ctx.context.playlistId,
|
||||||
songId,
|
songId,
|
||||||
},
|
},
|
||||||
|
serverId: ctx.data?.[0]?.serverId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
@@ -465,6 +466,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||||||
}, [
|
}, [
|
||||||
ctx.context?.playlistId,
|
ctx.context?.playlistId,
|
||||||
ctx.context?.tableRef,
|
ctx.context?.tableRef,
|
||||||
|
ctx.data,
|
||||||
ctx.dataNodes,
|
ctx.dataNodes,
|
||||||
removeFromPlaylistMutation,
|
removeFromPlaylistMutation,
|
||||||
serverType,
|
serverType,
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export const LeftControls = () => {
|
|||||||
onClick={handleToggleFullScreenPlayer}
|
onClick={handleToggleFullScreenPlayer}
|
||||||
>
|
>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
label="Open fullscreen player"
|
label="Toggle fullscreen player"
|
||||||
openDelay={500}
|
openDelay={500}
|
||||||
>
|
>
|
||||||
{currentSong?.imageUrl ? (
|
{currentSong?.imageUrl ? (
|
||||||
|
|||||||
@@ -112,6 +112,10 @@ export const useHandlePlayQueueAdd = () => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearTimeout(timeoutIds.current[fetchId] as ReturnType<typeof setTimeout>);
|
||||||
|
delete timeoutIds.current[fetchId];
|
||||||
|
toast.hide(fetchId);
|
||||||
|
|
||||||
return toast.error({
|
return toast.error({
|
||||||
message: err.message,
|
message: err.message,
|
||||||
title: 'Play queue add failed',
|
title: 'Play queue add failed',
|
||||||
|
|||||||
@@ -213,7 +213,11 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten
|
|||||||
const handleRowDoubleClick = (e: RowDoubleClickedEvent<QueueSong>) => {
|
const handleRowDoubleClick = (e: RowDoubleClickedEvent<QueueSong>) => {
|
||||||
if (!e.data) return;
|
if (!e.data) return;
|
||||||
handlePlayQueueAdd?.({
|
handlePlayQueueAdd?.({
|
||||||
byData: [e.data],
|
byItemType: {
|
||||||
|
id: [playlistId],
|
||||||
|
type: LibraryItem.PLAYLIST,
|
||||||
|
},
|
||||||
|
initialSongId: e.data.id,
|
||||||
playType: playButtonBehavior,
|
playType: playButtonBehavior,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
+54
-4
@@ -2,6 +2,7 @@ import { useCallback, ChangeEvent, MutableRefObject, MouseEvent } from 'react';
|
|||||||
import { IDatasource } from '@ag-grid-community/core';
|
import { IDatasource } from '@ag-grid-community/core';
|
||||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||||
import { Flex, Group, Stack } from '@mantine/core';
|
import { Flex, Group, Stack } from '@mantine/core';
|
||||||
|
import { closeAllModals, openModal } from '@mantine/modals';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
RiSortAsc,
|
RiSortAsc,
|
||||||
@@ -18,7 +19,16 @@ import {
|
|||||||
import { api } from '/@/renderer/api';
|
import { api } from '/@/renderer/api';
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { LibraryItem, PlaylistSongListQuery, SongListSort, SortOrder } from '/@/renderer/api/types';
|
import { LibraryItem, PlaylistSongListQuery, SongListSort, SortOrder } from '/@/renderer/api/types';
|
||||||
import { DropdownMenu, Button, Slider, MultiSelect, Switch, Text } from '/@/renderer/components';
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
Button,
|
||||||
|
Slider,
|
||||||
|
MultiSelect,
|
||||||
|
Switch,
|
||||||
|
Text,
|
||||||
|
ConfirmModal,
|
||||||
|
toast,
|
||||||
|
} from '/@/renderer/components';
|
||||||
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||||
import { useContainerQuery } from '/@/renderer/hooks';
|
import { useContainerQuery } from '/@/renderer/hooks';
|
||||||
import {
|
import {
|
||||||
@@ -34,6 +44,8 @@ import { ListDisplayType, ServerType, Play, TableColumn } from '/@/renderer/type
|
|||||||
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
|
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
import { SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
import { SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
||||||
|
import { openUpdatePlaylistModal } from '/@/renderer/features/playlists/components/update-playlist-form';
|
||||||
|
import { useDeletePlaylist } from '/@/renderer/features/playlists/mutations/delete-playlist-mutation';
|
||||||
|
|
||||||
const FILTERS = {
|
const FILTERS = {
|
||||||
jellyfin: [
|
jellyfin: [
|
||||||
@@ -238,6 +250,40 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deletePlaylistMutation = useDeletePlaylist({});
|
||||||
|
|
||||||
|
const handleDeletePlaylist = useCallback(() => {
|
||||||
|
if (!detailQuery.data) return;
|
||||||
|
deletePlaylistMutation?.mutate(
|
||||||
|
{ query: { id: detailQuery.data.id }, serverId: detailQuery.data.id },
|
||||||
|
{
|
||||||
|
onError: (err) => {
|
||||||
|
toast.error({
|
||||||
|
message: err.message,
|
||||||
|
title: 'Error deleting playlist',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success({
|
||||||
|
message: `Playlist has been deleted`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
closeAllModals();
|
||||||
|
}, [deletePlaylistMutation, detailQuery.data]);
|
||||||
|
|
||||||
|
const openDeletePlaylistModal = () => {
|
||||||
|
openModal({
|
||||||
|
children: (
|
||||||
|
<ConfirmModal onConfirm={handleDeletePlaylist}>
|
||||||
|
<Text>Are you sure you want to delete this playlist?</Text>
|
||||||
|
</ConfirmModal>
|
||||||
|
),
|
||||||
|
title: 'Delete playlist(s)',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex justify="space-between">
|
<Flex justify="space-between">
|
||||||
<Group
|
<Group
|
||||||
@@ -320,16 +366,20 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Divider />
|
<DropdownMenu.Divider />
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
disabled
|
|
||||||
icon={<RiEditFill />}
|
icon={<RiEditFill />}
|
||||||
onClick={() => handlePlay(Play.LAST)}
|
onClick={() =>
|
||||||
|
openUpdatePlaylistModal({
|
||||||
|
playlist: detailQuery.data!,
|
||||||
|
server: server!,
|
||||||
|
})
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Edit playlist
|
Edit playlist
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
disabled
|
disabled
|
||||||
icon={<RiDeleteBinFill />}
|
icon={<RiDeleteBinFill />}
|
||||||
onClick={() => handlePlay(Play.LAST)}
|
onClick={openDeletePlaylistModal}
|
||||||
>
|
>
|
||||||
Delete playlist
|
Delete playlist
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|||||||
@@ -184,6 +184,10 @@ export const PlaylistListHeaderFilters = ({ tableRef }: PlaylistListHeaderFilter
|
|||||||
setTable({ rowHeight: e });
|
setTable({ rowHeight: e });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
tableRef?.current?.api?.purgeInfiniteCache();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex justify="space-between">
|
<Flex justify="space-between">
|
||||||
<Group
|
<Group
|
||||||
@@ -246,7 +250,12 @@ export const PlaylistListHeaderFilters = ({ tableRef }: PlaylistListHeaderFilter
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenu.Target>
|
</DropdownMenu.Target>
|
||||||
<DropdownMenu.Dropdown>
|
<DropdownMenu.Dropdown>
|
||||||
<DropdownMenu.Item icon={<RiRefreshLine />}>Refresh</DropdownMenu.Item>
|
<DropdownMenu.Item
|
||||||
|
icon={<RiRefreshLine />}
|
||||||
|
onClick={handleRefresh}
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</DropdownMenu.Item>
|
||||||
</DropdownMenu.Dropdown>
|
</DropdownMenu.Dropdown>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const PlaylistListHeader = ({ itemCount, tableRef }: PlaylistListHeaderPr
|
|||||||
onClose: () => {
|
onClose: () => {
|
||||||
tableRef?.current?.api?.purgeInfiniteCache();
|
tableRef?.current?.api?.purgeInfiniteCache();
|
||||||
},
|
},
|
||||||
size: server?.type === ServerType?.NAVIDROME ? 'lg' : 'sm',
|
size: server?.type === ServerType?.NAVIDROME ? 'xl' : 'sm',
|
||||||
title: 'Create Playlist',
|
title: 'Create Playlist',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,9 +9,15 @@ interface SaveAsPlaylistFormProps {
|
|||||||
body: Partial<CreatePlaylistBody>;
|
body: Partial<CreatePlaylistBody>;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onSuccess: (data: CreatePlaylistResponse) => void;
|
onSuccess: (data: CreatePlaylistResponse) => void;
|
||||||
|
serverId: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SaveAsPlaylistForm = ({ body, onSuccess, onCancel }: SaveAsPlaylistFormProps) => {
|
export const SaveAsPlaylistForm = ({
|
||||||
|
body,
|
||||||
|
serverId,
|
||||||
|
onSuccess,
|
||||||
|
onCancel,
|
||||||
|
}: SaveAsPlaylistFormProps) => {
|
||||||
const mutation = useCreatePlaylist({});
|
const mutation = useCreatePlaylist({});
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
|
|
||||||
@@ -31,7 +37,7 @@ export const SaveAsPlaylistForm = ({ body, onSuccess, onCancel }: SaveAsPlaylist
|
|||||||
|
|
||||||
const handleSubmit = form.onSubmit((values) => {
|
const handleSubmit = form.onSubmit((values) => {
|
||||||
mutation.mutate(
|
mutation.mutate(
|
||||||
{ body: values },
|
{ body: values, serverId },
|
||||||
{
|
{
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
toast.error({ message: err.message, title: 'Error creating playlist' });
|
toast.error({ message: err.message, title: 'Error creating playlist' });
|
||||||
|
|||||||
@@ -1,8 +1,22 @@
|
|||||||
import { Group, Stack } from '@mantine/core';
|
import { Group, Stack } from '@mantine/core';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { ServerType, UpdatePlaylistBody, UpdatePlaylistQuery, User } from '/@/renderer/api/types';
|
import { openModal, closeAllModals } from '@mantine/modals';
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import {
|
||||||
|
PlaylistDetailResponse,
|
||||||
|
ServerListItem,
|
||||||
|
ServerType,
|
||||||
|
SortOrder,
|
||||||
|
UpdatePlaylistBody,
|
||||||
|
UpdatePlaylistQuery,
|
||||||
|
User,
|
||||||
|
UserListQuery,
|
||||||
|
UserListSort,
|
||||||
|
} from '/@/renderer/api/types';
|
||||||
import { Button, Select, Switch, TextInput, toast } from '/@/renderer/components';
|
import { Button, Select, Switch, TextInput, toast } from '/@/renderer/components';
|
||||||
import { useUpdatePlaylist } from '/@/renderer/features/playlists/mutations/update-playlist-mutation';
|
import { useUpdatePlaylist } from '/@/renderer/features/playlists/mutations/update-playlist-mutation';
|
||||||
|
import { queryClient } from '/@/renderer/lib/react-query';
|
||||||
import { useCurrentServer } from '/@/renderer/store';
|
import { useCurrentServer } from '/@/renderer/store';
|
||||||
|
|
||||||
interface UpdatePlaylistFormProps {
|
interface UpdatePlaylistFormProps {
|
||||||
@@ -103,3 +117,49 @@ export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlayl
|
|||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const openUpdatePlaylistModal = async (args: {
|
||||||
|
playlist: PlaylistDetailResponse;
|
||||||
|
server: ServerListItem;
|
||||||
|
}) => {
|
||||||
|
const { playlist, server } = args;
|
||||||
|
|
||||||
|
const query: UserListQuery = {
|
||||||
|
sortBy: UserListSort.NAME,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
startIndex: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!server) return;
|
||||||
|
|
||||||
|
const users = await queryClient.fetchQuery({
|
||||||
|
queryFn: ({ signal }) =>
|
||||||
|
api.controller.getUserList({ apiClientProps: { server, signal }, query }),
|
||||||
|
queryKey: queryKeys.users.list(server?.id || '', query),
|
||||||
|
});
|
||||||
|
|
||||||
|
openModal({
|
||||||
|
children: (
|
||||||
|
<UpdatePlaylistForm
|
||||||
|
body={{
|
||||||
|
_custom: {
|
||||||
|
navidrome: {
|
||||||
|
owner: playlist?.owner || undefined,
|
||||||
|
ownerId: playlist?.ownerId || undefined,
|
||||||
|
public: playlist?.public || false,
|
||||||
|
rules: playlist?.rules || undefined,
|
||||||
|
sync: playlist?.sync || undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
comment: playlist?.description || undefined,
|
||||||
|
genres: playlist?.genres,
|
||||||
|
name: playlist?.name,
|
||||||
|
}}
|
||||||
|
query={{ id: playlist?.id }}
|
||||||
|
users={users?.items}
|
||||||
|
onCancel={closeAllModals}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
title: 'Edit playlist',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ const PlaylistDetailSongListRoute = () => {
|
|||||||
comment: detailQuery?.data?.description || '',
|
comment: detailQuery?.data?.description || '',
|
||||||
name: detailQuery?.data?.name,
|
name: detailQuery?.data?.name,
|
||||||
},
|
},
|
||||||
|
serverId: detailQuery?.data?.serverId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
@@ -64,13 +65,19 @@ const PlaylistDetailSongListRoute = () => {
|
|||||||
navigate(generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId: data?.id || '' }), {
|
navigate(generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId: data?.id || '' }), {
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
deletePlaylistMutation.mutate({ query: { id: playlistId } });
|
deletePlaylistMutation.mutate({
|
||||||
|
query: { id: playlistId },
|
||||||
|
serverId: detailQuery?.data?.serverId,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveAs = (filter: Record<string, any>) => {
|
const handleSaveAs = (
|
||||||
|
filter: Record<string, any>,
|
||||||
|
extraFilters: { limit?: number; sortBy?: string; sortOrder?: string },
|
||||||
|
) => {
|
||||||
openModal({
|
openModal({
|
||||||
children: (
|
children: (
|
||||||
<SaveAsPlaylistForm
|
<SaveAsPlaylistForm
|
||||||
@@ -82,8 +89,9 @@ const PlaylistDetailSongListRoute = () => {
|
|||||||
public: detailQuery?.data?.public || false,
|
public: detailQuery?.data?.public || false,
|
||||||
rules: {
|
rules: {
|
||||||
...filter,
|
...filter,
|
||||||
order: 'desc',
|
limit: extraFilters.limit || undefined,
|
||||||
sort: 'year',
|
order: extraFilters.sortOrder || 'desc',
|
||||||
|
sort: extraFilters.sortBy || 'dateAdded',
|
||||||
},
|
},
|
||||||
sync: detailQuery?.data?.sync || false,
|
sync: detailQuery?.data?.sync || false,
|
||||||
},
|
},
|
||||||
@@ -91,6 +99,7 @@ const PlaylistDetailSongListRoute = () => {
|
|||||||
comment: detailQuery?.data?.description || '',
|
comment: detailQuery?.data?.description || '',
|
||||||
name: detailQuery?.data?.name,
|
name: detailQuery?.data?.name,
|
||||||
}}
|
}}
|
||||||
|
serverId={detailQuery?.data?.serverId}
|
||||||
onCancel={closeAllModals}
|
onCancel={closeAllModals}
|
||||||
onSuccess={(data) =>
|
onSuccess={(data) =>
|
||||||
navigate(generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId: data?.id || '' }))
|
navigate(generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId: data?.id || '' }))
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { data, isLoading } = useSearch({
|
const { data, isLoading } = useSearch({
|
||||||
options: { enabled: debouncedQuery !== '' && query !== '' },
|
options: { enabled: isHome && debouncedQuery !== '' && query !== '' },
|
||||||
query: {
|
query: {
|
||||||
albumArtistLimit: 4,
|
albumArtistLimit: 4,
|
||||||
albumArtistStartIndex: 0,
|
albumArtistStartIndex: 0,
|
||||||
@@ -58,9 +58,9 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
|
|||||||
serverId: server?.id,
|
serverId: server?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const showAlbumGroup = Boolean(query && data && data?.albums?.length > 0);
|
const showAlbumGroup = isHome && Boolean(query && data && data?.albums?.length > 0);
|
||||||
const showArtistGroup = Boolean(query && data && data?.albumArtists?.length > 0);
|
const showArtistGroup = isHome && Boolean(query && data && data?.albumArtists?.length > 0);
|
||||||
const showTrackGroup = Boolean(query && data && data?.songs?.length > 0);
|
const showTrackGroup = isHome && Boolean(query && data && data?.songs?.length > 0);
|
||||||
|
|
||||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||||
|
|
||||||
@@ -238,7 +238,7 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
|
|||||||
p="0.5rem"
|
p="0.5rem"
|
||||||
>
|
>
|
||||||
<Group position="apart">
|
<Group position="apart">
|
||||||
<Command.Loading>{isLoading && query !== '' && <Spinner />}</Command.Loading>
|
<Command.Loading>{isHome && isLoading && query !== '' && <Spinner />}</Command.Loading>
|
||||||
<Group spacing="sm">
|
<Group spacing="sm">
|
||||||
<Kbd size="md">ESC</Kbd>
|
<Kbd size="md">ESC</Kbd>
|
||||||
<Kbd size="md">↑</Kbd>
|
<Kbd size="md">↑</Kbd>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export const HomeCommands = ({
|
|||||||
|
|
||||||
openModal({
|
openModal({
|
||||||
children: <CreatePlaylistForm onCancel={() => closeAllModals()} />,
|
children: <CreatePlaylistForm onCancel={() => closeAllModals()} />,
|
||||||
size: server?.type === ServerType?.NAVIDROME ? 'lg' : 'sm',
|
size: server?.type === ServerType?.NAVIDROME ? 'xl' : 'sm',
|
||||||
title: 'Create Playlist',
|
title: 'Create Playlist',
|
||||||
});
|
});
|
||||||
}, [handleClose, server?.type]);
|
}, [handleClose, server?.type]);
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ const Container = styled(Flex)<{ $active?: boolean; $disabled?: boolean }>`
|
|||||||
border-right: var(--sidebar-border);
|
border-right: var(--sidebar-border);
|
||||||
cursor: ${(props) => (props.$disabled ? 'default' : 'pointer')};
|
cursor: ${(props) => (props.$disabled ? 'default' : 'pointer')};
|
||||||
opacity: ${(props) => props.$disabled && 0.6};
|
opacity: ${(props) => props.$disabled && 0.6};
|
||||||
|
user-select: ${(props) => (props.$disabled ? 'none' : 'initial')};
|
||||||
|
pointer-events: ${(props) => (props.$disabled ? 'none' : 'all')};
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
fill: ${(props) => (props.$active ? 'var(--primary-color)' : 'var(--sidebar-fg)')};
|
fill: ${(props) => (props.$active ? 'var(--primary-color)' : 'var(--sidebar-fg)')};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { MouseEvent } from 'react';
|
|||||||
import { Stack, Accordion, Center, Group, Divider, Box } from '@mantine/core';
|
import { Stack, Accordion, Center, Group, Divider, Box } from '@mantine/core';
|
||||||
import { closeAllModals, openModal } from '@mantine/modals';
|
import { closeAllModals, openModal } from '@mantine/modals';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { Button, MotionStack, Spinner } from '/@/renderer/components';
|
import { Button, MotionStack, Spinner, Tooltip } from '/@/renderer/components';
|
||||||
import {
|
import {
|
||||||
RiAddFill,
|
RiAddFill,
|
||||||
RiAlbumFill,
|
RiAlbumFill,
|
||||||
@@ -95,7 +95,7 @@ export const Sidebar = () => {
|
|||||||
|
|
||||||
openModal({
|
openModal({
|
||||||
children: <CreatePlaylistForm onCancel={() => closeAllModals()} />,
|
children: <CreatePlaylistForm onCancel={() => closeAllModals()} />,
|
||||||
size: server?.type === ServerType?.NAVIDROME ? 'lg' : 'sm',
|
size: server?.type === ServerType?.NAVIDROME ? 'xl' : 'sm',
|
||||||
title: 'Create Playlist',
|
title: 'Create Playlist',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -288,19 +288,24 @@ export const Sidebar = () => {
|
|||||||
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
||||||
onClick={expandFullScreenPlayer}
|
onClick={expandFullScreenPlayer}
|
||||||
>
|
>
|
||||||
{upsizedImageUrl ? (
|
<Tooltip
|
||||||
<SidebarImage
|
label="Toggle fullscreen player"
|
||||||
loading="eager"
|
openDelay={500}
|
||||||
src={upsizedImageUrl}
|
>
|
||||||
/>
|
{upsizedImageUrl ? (
|
||||||
) : (
|
<SidebarImage
|
||||||
<Center sx={{ background: 'var(--placeholder-bg)', height: '100%' }}>
|
loading="eager"
|
||||||
<RiDiscLine
|
src={upsizedImageUrl}
|
||||||
color="var(--placeholder-fg)"
|
|
||||||
size={50}
|
|
||||||
/>
|
/>
|
||||||
</Center>
|
) : (
|
||||||
)}
|
<Center sx={{ background: 'var(--placeholder-bg)', height: '100%' }}>
|
||||||
|
<RiDiscLine
|
||||||
|
color="var(--placeholder-fg)"
|
||||||
|
size={50}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
opacity={0.8}
|
opacity={0.8}
|
||||||
|
|||||||
@@ -107,21 +107,31 @@ export const usePlayerStore = create<PlayerSlice>()(
|
|||||||
|
|
||||||
if (playType === Play.NOW) {
|
if (playType === Play.NOW) {
|
||||||
if (get().shuffle === PlayerShuffle.TRACK) {
|
if (get().shuffle === PlayerShuffle.TRACK) {
|
||||||
const shuffledSongs = shuffle(queueSongs);
|
|
||||||
const index = initialIndex || 0;
|
const index = initialIndex || 0;
|
||||||
const initialSongUniqueId = queueSongs[index].uniqueId;
|
const initialSong = queueSongs[index];
|
||||||
const initialSongIndex = shuffledSongs.findIndex(
|
const queueCopy = [...queueSongs];
|
||||||
(song) => song.uniqueId === initialSongUniqueId,
|
|
||||||
|
// Splice the initial song from the queue
|
||||||
|
queueCopy.splice(index, 1);
|
||||||
|
|
||||||
|
const shuffledSongIndicesWithoutInitial = shuffle(queueCopy).map(
|
||||||
|
(song) => song.uniqueId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Add the initial song to the start of the shuffled queue
|
||||||
|
const shuffledSongIndices = [
|
||||||
|
initialSong.uniqueId,
|
||||||
|
...shuffledSongIndicesWithoutInitial,
|
||||||
|
];
|
||||||
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.queue.shuffled = shuffledSongs.map((song) => song.uniqueId);
|
state.queue.shuffled = shuffledSongIndices;
|
||||||
state.queue.default = queueSongs;
|
state.queue.default = queueSongs;
|
||||||
state.current.time = 0;
|
state.current.time = 0;
|
||||||
state.current.player = 1;
|
state.current.player = 1;
|
||||||
state.current.index = initialSongIndex;
|
state.current.index = 0;
|
||||||
state.current.shuffledIndex = 0;
|
state.current.shuffledIndex = 0;
|
||||||
state.current.song = shuffledSongs[initialSongIndex];
|
state.current.song = initialSong;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const index = initialIndex || 0;
|
const index = initialIndex || 0;
|
||||||
@@ -849,7 +859,7 @@ export const usePlayerStore = create<PlayerSlice>()(
|
|||||||
},
|
},
|
||||||
name: 'store_player',
|
name: 'store_player',
|
||||||
partialize: (state) => {
|
partialize: (state) => {
|
||||||
const notPersisted = ['queue', 'current'];
|
const notPersisted = ['queue', 'current', 'entry'];
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.entries(state).filter(([key]) => !notPersisted.includes(key)),
|
Object.entries(state).filter(([key]) => !notPersisted.includes(key)),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ const initialState: SettingsState = {
|
|||||||
volumeMute: { allowGlobal: true, hotkey: '', isGlobal: false },
|
volumeMute: { allowGlobal: true, hotkey: '', isGlobal: false },
|
||||||
volumeUp: { allowGlobal: true, hotkey: '', isGlobal: false },
|
volumeUp: { allowGlobal: true, hotkey: '', isGlobal: false },
|
||||||
},
|
},
|
||||||
globalMediaHotkeys: false,
|
globalMediaHotkeys: true,
|
||||||
},
|
},
|
||||||
playback: {
|
playback: {
|
||||||
audioDeviceId: undefined,
|
audioDeviceId: undefined,
|
||||||
|
|||||||
Reference in New Issue
Block a user