Compare commits

...

13 Commits

Author SHA1 Message Date
jeffvli 3675146f1f Fix opacity mask for unsynced lyrics container 2023-10-07 19:58:04 -07:00
jeffvli 946f4ff306 Bump to v0.4.1 2023-10-07 19:06:30 -07:00
jeffvli 277669c413 Fix album detail table customizations 2023-10-07 18:11:02 -07:00
jeffvli 49b6478b72 Fix table row actions button on album detail and play queue 2023-10-07 17:32:59 -07:00
jeffvli ca39409cc3 Respect order of set-queue function (fix race condition) 2023-10-07 16:46:23 -07:00
jeffvli cca6fa21db Adjust scrobble duration to check in ms 2023-10-05 22:11:48 -07:00
jeffvli 5e1059870c Fix second song on startup not playing 2023-10-05 21:54:11 -07:00
Kendall Garner 6bac172bbe fix scrobble durations (#269)
* fix scrobble durations

* Fix scrobble condition on last song in queue, normalize ms

---------

Co-authored-by: jeffvli <jeffvictorli@gmail.com>
2023-10-05 21:45:47 -07:00
Kendall Garner 118a9f73d1 fix unsynced lyrics (#279) 2023-10-04 22:02:42 -07:00
jeffvli c464be8cea Fix quit functionality (#184) 2023-09-27 02:37:03 -07:00
jeffvli 3bbe696f4c Update react-router and add useTransition support 2023-09-25 16:13:27 -07:00
jeffvli f7cacd2b73 Remove page fade in transition 2023-09-25 16:12:51 -07:00
jeffvli 62794623a3 Fix tracks list refresh on search 2023-09-25 15:57:48 -07:00
18 changed files with 183 additions and 101 deletions
+31 -31
View File
@@ -1,12 +1,12 @@
{ {
"name": "feishin", "name": "feishin",
"version": "0.4.0", "version": "0.4.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "feishin", "name": "feishin",
"version": "0.4.0", "version": "0.4.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
@@ -59,8 +59,8 @@
"react-i18next": "^11.16.7", "react-i18next": "^11.16.7",
"react-icons": "^4.10.1", "react-icons": "^4.10.1",
"react-player": "^2.11.0", "react-player": "^2.11.0",
"react-router": "^6.5.0", "react-router": "^6.16.0",
"react-router-dom": "^6.5.0", "react-router-dom": "^6.16.0",
"react-simple-img": "^3.0.0", "react-simple-img": "^3.0.0",
"react-virtualized-auto-sizer": "^1.0.17", "react-virtualized-auto-sizer": "^1.0.17",
"react-window": "^1.8.9", "react-window": "^1.8.9",
@@ -4336,11 +4336,11 @@
} }
}, },
"node_modules/@remix-run/router": { "node_modules/@remix-run/router": {
"version": "1.1.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.9.0.tgz",
"integrity": "sha512-rGl+jH/7x1KBCQScz9p54p0dtPLNeKGb3e0wD2H5/oZj41bwQUnXdzbj2TbUAFhvD7cp9EyEQA4dEgpUFa1O7Q==", "integrity": "sha512-bV63itrKBC0zdT27qYm6SDZHlkXwFL1xMBuhkn+X7l0+IIhNaH5wuuvZKp6eKhCD4KFhujhfhCT1YxXW6esUIA==",
"engines": { "engines": {
"node": ">=14" "node": ">=14.0.0"
} }
}, },
"node_modules/@sindresorhus/is": { "node_modules/@sindresorhus/is": {
@@ -17055,29 +17055,29 @@
} }
}, },
"node_modules/react-router": { "node_modules/react-router": {
"version": "6.5.0", "version": "6.16.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.5.0.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.16.0.tgz",
"integrity": "sha512-fqqUSU0NC0tSX0sZbyuxzuAzvGqbjiZItBQnyicWlOUmzhAU8YuLgRbaCL2hf3sJdtRy4LP/WBrWtARkMvdGPQ==", "integrity": "sha512-VT4Mmc4jj5YyjpOi5jOf0I+TYzGpvzERy4ckNSvSh2RArv8LLoCxlsZ2D+tc7zgjxcY34oTz2hZaeX5RVprKqA==",
"dependencies": { "dependencies": {
"@remix-run/router": "1.1.0" "@remix-run/router": "1.9.0"
}, },
"engines": { "engines": {
"node": ">=14" "node": ">=14.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"react": ">=16.8" "react": ">=16.8"
} }
}, },
"node_modules/react-router-dom": { "node_modules/react-router-dom": {
"version": "6.5.0", "version": "6.16.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.5.0.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.16.0.tgz",
"integrity": "sha512-/XzRc5fq80gW1ctiIGilyKFZC/j4kfe75uivMsTChFbkvrK4ZrF3P3cGIc1f/SSkQ4JiJozPrf+AwUHHWVehVg==", "integrity": "sha512-aTfBLv3mk/gaKLxgRDUPbPw+s4Y/O+ma3rEN1u8EgEpLpPe6gNjIsWt9rxushMHHMb7mSwxRGdGlGdvmFsyPIg==",
"dependencies": { "dependencies": {
"@remix-run/router": "1.1.0", "@remix-run/router": "1.9.0",
"react-router": "6.5.0" "react-router": "6.16.0"
}, },
"engines": { "engines": {
"node": ">=14" "node": ">=14.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"react": ">=16.8", "react": ">=16.8",
@@ -24366,9 +24366,9 @@
} }
}, },
"@remix-run/router": { "@remix-run/router": {
"version": "1.1.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.9.0.tgz",
"integrity": "sha512-rGl+jH/7x1KBCQScz9p54p0dtPLNeKGb3e0wD2H5/oZj41bwQUnXdzbj2TbUAFhvD7cp9EyEQA4dEgpUFa1O7Q==" "integrity": "sha512-bV63itrKBC0zdT27qYm6SDZHlkXwFL1xMBuhkn+X7l0+IIhNaH5wuuvZKp6eKhCD4KFhujhfhCT1YxXW6esUIA=="
}, },
"@sindresorhus/is": { "@sindresorhus/is": {
"version": "4.6.0", "version": "4.6.0",
@@ -33977,20 +33977,20 @@
} }
}, },
"react-router": { "react-router": {
"version": "6.5.0", "version": "6.16.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.5.0.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.16.0.tgz",
"integrity": "sha512-fqqUSU0NC0tSX0sZbyuxzuAzvGqbjiZItBQnyicWlOUmzhAU8YuLgRbaCL2hf3sJdtRy4LP/WBrWtARkMvdGPQ==", "integrity": "sha512-VT4Mmc4jj5YyjpOi5jOf0I+TYzGpvzERy4ckNSvSh2RArv8LLoCxlsZ2D+tc7zgjxcY34oTz2hZaeX5RVprKqA==",
"requires": { "requires": {
"@remix-run/router": "1.1.0" "@remix-run/router": "1.9.0"
} }
}, },
"react-router-dom": { "react-router-dom": {
"version": "6.5.0", "version": "6.16.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.5.0.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.16.0.tgz",
"integrity": "sha512-/XzRc5fq80gW1ctiIGilyKFZC/j4kfe75uivMsTChFbkvrK4ZrF3P3cGIc1f/SSkQ4JiJozPrf+AwUHHWVehVg==", "integrity": "sha512-aTfBLv3mk/gaKLxgRDUPbPw+s4Y/O+ma3rEN1u8EgEpLpPe6gNjIsWt9rxushMHHMb7mSwxRGdGlGdvmFsyPIg==",
"requires": { "requires": {
"@remix-run/router": "1.1.0", "@remix-run/router": "1.9.0",
"react-router": "6.5.0" "react-router": "6.16.0"
} }
}, },
"react-shallow-renderer": { "react-shallow-renderer": {
+3 -3
View File
@@ -2,7 +2,7 @@
"name": "feishin", "name": "feishin",
"productName": "Feishin", "productName": "Feishin",
"description": "Feishin music server", "description": "Feishin music server",
"version": "0.4.0", "version": "0.4.1",
"scripts": { "scripts": {
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\" \"npm run build:remote\"", "build": "concurrently \"npm run build:main\" \"npm run build:renderer\" \"npm run build:remote\"",
"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",
@@ -305,8 +305,8 @@
"react-i18next": "^11.16.7", "react-i18next": "^11.16.7",
"react-icons": "^4.10.1", "react-icons": "^4.10.1",
"react-player": "^2.11.0", "react-player": "^2.11.0",
"react-router": "^6.5.0", "react-router": "^6.16.0",
"react-router-dom": "^6.5.0", "react-router-dom": "^6.16.0",
"react-simple-img": "^3.0.0", "react-simple-img": "^3.0.0",
"react-virtualized-auto-sizer": "^1.0.17", "react-virtualized-auto-sizer": "^1.0.17",
"react-window": "^1.8.9", "react-window": "^1.8.9",
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "feishin", "name": "feishin",
"version": "0.4.0", "version": "0.4.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "feishin", "name": "feishin",
"version": "0.4.0", "version": "0.4.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "feishin", "name": "feishin",
"version": "0.4.0", "version": "0.4.1",
"description": "", "description": "",
"main": "./dist/main/main.js", "main": "./dist/main/main.js",
"author": { "author": {
+5 -7
View File
@@ -112,18 +112,16 @@ ipcMain.on('player-set-queue', async (_event, data: PlayerData, pause?: boolean)
try { try {
if (data.queue.current) { if (data.queue.current) {
getMpvInstance() await getMpvInstance()
?.load(data.queue.current.streamUrl, 'replace') ?.load(data.queue.current.streamUrl, 'replace')
.then(() => {
// eslint-disable-next-line promise/always-return
if (data.queue.next) {
getMpvInstance()?.load(data.queue.next.streamUrl, 'append');
}
})
.catch((err) => { .catch((err) => {
console.log('MPV failed to load song', err); console.log('MPV failed to load song', err);
getMpvInstance()?.play(); getMpvInstance()?.play();
}); });
if (data.queue.next) {
await getMpvInstance()?.load(data.queue.next.streamUrl, 'append');
}
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);
+5
View File
@@ -259,6 +259,11 @@ const createWindow = async () => {
mainWindow?.close(); mainWindow?.close();
}); });
ipcMain.on('window-quit', () => {
mainWindow?.close();
app.exit();
});
ipcMain.on('app-restart', () => { ipcMain.on('app-restart', () => {
// Fix for .AppImage // Fix for .AppImage
if (process.env.APPIMAGE) { if (process.env.APPIMAGE) {
+8
View File
@@ -3,16 +3,23 @@ import { ipcRenderer } from 'electron';
const exit = () => { const exit = () => {
ipcRenderer.send('window-close'); ipcRenderer.send('window-close');
}; };
const maximize = () => { const maximize = () => {
ipcRenderer.send('window-maximize'); ipcRenderer.send('window-maximize');
}; };
const minimize = () => { const minimize = () => {
ipcRenderer.send('window-minimize'); ipcRenderer.send('window-minimize');
}; };
const unmaximize = () => { const unmaximize = () => {
ipcRenderer.send('window-unmaximize'); ipcRenderer.send('window-unmaximize');
}; };
const quit = () => {
ipcRenderer.send('window-quit');
};
const devtools = () => { const devtools = () => {
ipcRenderer.send('window-dev-tools'); ipcRenderer.send('window-dev-tools');
}; };
@@ -22,5 +29,6 @@ export const browser = {
exit, exit,
maximize, maximize,
minimize, minimize,
quit,
unmaximize, unmaximize,
}; };
@@ -934,7 +934,7 @@ const getLyrics = async (args: LyricsArgs): Promise<LyricsResponse> => {
} }
if (res.body.Lyrics.length > 0 && res.body.Lyrics[0].Start === undefined) { if (res.body.Lyrics.length > 0 && res.body.Lyrics[0].Start === undefined) {
return res.body.Lyrics[0].Text; return res.body.Lyrics.map((lyric) => lyric.Text).join('\n');
} }
return res.body.Lyrics.map((lyric) => [lyric.Start! / 1e4, lyric.Text]); return res.body.Lyrics.map((lyric) => [lyric.Start! / 1e4, lyric.Text]);
@@ -87,6 +87,7 @@ export const GENRE_TABLE_COLUMNS = [
]; ];
interface TableConfigDropdownProps { interface TableConfigDropdownProps {
// tableRef?: MutableRefObject<AgGridReactType<any> | null>;
type: TableType; type: TableType;
} }
@@ -12,9 +12,9 @@ import { AlbumListSort, LibraryItem, QueueSong, SortOrder } from '/@/renderer/ap
import { Button, Popover } from '/@/renderer/components'; import { Button, Popover } from '/@/renderer/components';
import { MemoizedSwiperGridCarousel } from '/@/renderer/components/grid-carousel'; import { MemoizedSwiperGridCarousel } from '/@/renderer/components/grid-carousel';
import { import {
getColumnDefs,
TableConfigDropdown, TableConfigDropdown,
VirtualTable, VirtualTable,
getColumnDefs,
} from '/@/renderer/components/virtual-table'; } from '/@/renderer/components/virtual-table';
import { FullWidthDiscCell } from '/@/renderer/components/virtual-table/cells/full-width-disc-cell'; import { FullWidthDiscCell } from '/@/renderer/components/virtual-table/cells/full-width-disc-cell';
import { useCurrentSongRowStyles } from '/@/renderer/components/virtual-table/hooks/use-current-song-row-styles'; import { useCurrentSongRowStyles } from '/@/renderer/components/virtual-table/hooks/use-current-song-row-styles';
@@ -34,7 +34,11 @@ import { LibraryBackgroundOverlay } from '/@/renderer/features/shared/components
import { useContainerQuery } from '/@/renderer/hooks'; import { useContainerQuery } from '/@/renderer/hooks';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer } from '/@/renderer/store'; import { useCurrentServer } from '/@/renderer/store';
import { usePlayButtonBehavior, useTableSettings } from '/@/renderer/store/settings.store'; import {
usePlayButtonBehavior,
useSettingsStoreActions,
useTableSettings,
} from '/@/renderer/store/settings.store';
import { Play } from '/@/renderer/types'; import { Play } from '/@/renderer/types';
const isFullWidthRow = (node: RowNode) => { const isFullWidthRow = (node: RowNode) => {
@@ -65,16 +69,20 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP
const cq = useContainerQuery(); const cq = useContainerQuery();
const handlePlayQueueAdd = usePlayQueueAdd(); const handlePlayQueueAdd = usePlayQueueAdd();
const tableConfig = useTableSettings('albumDetail'); const tableConfig = useTableSettings('albumDetail');
const { setTable } = useSettingsStoreActions();
const columnDefs = useMemo(() => getColumnDefs(tableConfig.columns), [tableConfig.columns]); const columnDefs = useMemo(() => getColumnDefs(tableConfig.columns), [tableConfig.columns]);
const getRowHeight = useCallback((params: RowHeightParams) => { const getRowHeight = useCallback(
(params: RowHeightParams) => {
if (isFullWidthRow(params.node)) { if (isFullWidthRow(params.node)) {
return 45; return 45;
} }
return 60; return tableConfig.rowHeight;
}, []); },
[tableConfig.rowHeight],
);
const songsRowData = useMemo(() => { const songsRowData = useMemo(() => {
if (!detailQuery.data?.songs) { if (!detailQuery.data?.songs) {
@@ -216,7 +224,7 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP
}); });
}; };
const handleContextMenu = useHandleTableContextMenu(LibraryItem.SONG, SONG_CONTEXT_MENU_ITEMS); const onCellContextMenu = useHandleTableContextMenu(LibraryItem.SONG, SONG_CONTEXT_MENU_ITEMS);
const handleRowDoubleClick = (e: RowDoubleClickedEvent<QueueSong>) => { const handleRowDoubleClick = (e: RowDoubleClickedEvent<QueueSong>) => {
if (!e.data || e.node.isFullWidthCell()) return; if (!e.data || e.node.isFullWidthCell()) return;
@@ -266,6 +274,32 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP
ALBUM_CONTEXT_MENU_ITEMS, ALBUM_CONTEXT_MENU_ITEMS,
); );
const onColumnMoved = useCallback(() => {
const { columnApi } = tableRef?.current || {};
const columnsOrder = columnApi?.getAllGridColumns();
if (!columnsOrder) return;
const columnsInSettings = tableConfig.columns;
const updatedColumns = [];
for (const column of columnsOrder) {
const columnInSettings = columnsInSettings.find(
(c) => c.column === column.getColDef().colId,
);
if (columnInSettings) {
updatedColumns.push({
...columnInSettings,
...(!tableConfig.autoFit && {
width: column.getActualWidth(),
}),
});
}
}
setTable('albumDetail', { ...tableConfig, columns: updatedColumns });
}, [setTable, tableConfig, tableRef]);
const { rowClassRules } = useCurrentSongRowStyles({ tableRef }); const { rowClassRules } = useCurrentSongRowStyles({ tableRef });
return ( return (
@@ -352,6 +386,7 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP
)} )}
<Box style={{ minHeight: '300px' }}> <Box style={{ minHeight: '300px' }}>
<VirtualTable <VirtualTable
key={`table-${tableConfig.rowHeight}`}
ref={tableRef} ref={tableRef}
autoHeight autoHeight
stickyHeader stickyHeader
@@ -360,6 +395,9 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP
suppressRowDrag suppressRowDrag
autoFitColumns={tableConfig.autoFit} autoFitColumns={tableConfig.autoFit}
columnDefs={columnDefs} columnDefs={columnDefs}
context={{
onCellContextMenu,
}}
enableCellChangeFlash={false} enableCellChangeFlash={false}
fullWidthCellRenderer={FullWidthDiscCell} fullWidthCellRenderer={FullWidthDiscCell}
getRowHeight={getRowHeight} getRowHeight={getRowHeight}
@@ -374,7 +412,8 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP
rowClassRules={rowClassRules} rowClassRules={rowClassRules}
rowData={songsRowData} rowData={songsRowData}
rowSelection="multiple" rowSelection="multiple"
onCellContextMenu={handleContextMenu} onCellContextMenu={onCellContextMenu}
onColumnMoved={onColumnMoved}
onRowDoubleClicked={handleRowDoubleClick} onRowDoubleClicked={handleRowDoubleClick}
/> />
</Box> </Box>
@@ -18,6 +18,14 @@ const UnsynchronizedLyricsContainer = styled.div<{ $gap: number }>`
overflow: scroll; overflow: scroll;
transform: translateY(-2rem); transform: translateY(-2rem);
-webkit-mask-image: linear-gradient(
180deg,
transparent 5%,
rgb(0 0 0 / 100%) 20%,
rgb(0 0 0 / 100%) 85%,
transparent 95%
);
mask-image: linear-gradient( mask-image: linear-gradient(
180deg, 180deg,
transparent 5%, transparent 5%,
@@ -206,7 +206,7 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref<any>) => {
} }
}, [currentSong, previousSong, tableConfig.followCurrentSong]); }, [currentSong, previousSong, tableConfig.followCurrentSong]);
const handleContextMenu = useHandleTableContextMenu(LibraryItem.SONG, QUEUE_CONTEXT_MENU_ITEMS); const onCellContextMenu = useHandleTableContextMenu(LibraryItem.SONG, QUEUE_CONTEXT_MENU_ITEMS);
return ( return (
<ErrorBoundary FallbackComponent={ErrorFallback}> <ErrorBoundary FallbackComponent={ErrorFallback}>
@@ -218,6 +218,9 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref<any>) => {
rowDragMultiRow rowDragMultiRow
autoFitColumns={tableConfig.autoFit} autoFitColumns={tableConfig.autoFit}
columnDefs={columnDefs} columnDefs={columnDefs}
context={{
onCellContextMenu,
}}
deselectOnClickOutside={type === 'fullScreen'} deselectOnClickOutside={type === 'fullScreen'}
getRowId={(data) => data.data.uniqueId} getRowId={(data) => data.data.uniqueId}
rowBuffer={50} rowBuffer={50}
@@ -225,7 +228,7 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref<any>) => {
rowData={queue} rowData={queue}
rowHeight={tableConfig.rowHeight || 40} rowHeight={tableConfig.rowHeight || 40}
suppressCellFocus={type === 'fullScreen'} suppressCellFocus={type === 'fullScreen'}
onCellContextMenu={handleContextMenu} onCellContextMenu={onCellContextMenu}
onCellDoubleClicked={handleDoubleClick} onCellDoubleClicked={handleDoubleClick}
onColumnMoved={handleColumnChange} onColumnMoved={handleColumnChange}
onColumnResized={debouncedColumnChange} onColumnResized={debouncedColumnChange}
@@ -34,20 +34,21 @@ Progress Events (Jellyfin only):
*/ */
const checkScrobbleConditions = (args: { const checkScrobbleConditions = (args: {
scrobbleAtDuration: number; scrobbleAtDurationMs: number;
scrobbleAtPercentage: number; scrobbleAtPercentage: number;
songCompletedDuration: number; songCompletedDurationMs: number;
songDuration: number; songDurationMs: number;
}) => { }) => {
const { scrobbleAtDuration, scrobbleAtPercentage, songCompletedDuration, songDuration } = args; const { scrobbleAtDurationMs, scrobbleAtPercentage, songCompletedDurationMs, songDurationMs } =
const percentageOfSongCompleted = songDuration args;
? (songCompletedDuration / songDuration) * 100 const percentageOfSongCompleted = songDurationMs
? (songCompletedDurationMs / songDurationMs) * 100
: 0; : 0;
return ( const shouldScrobbleBasedOnPercetange = percentageOfSongCompleted >= scrobbleAtPercentage;
percentageOfSongCompleted >= scrobbleAtPercentage || const shouldScrobbleBasedOnDuration = songCompletedDurationMs >= scrobbleAtDurationMs;
songCompletedDuration >= scrobbleAtDuration
); return shouldScrobbleBasedOnPercetange || shouldScrobbleBasedOnDuration;
}; };
export const useScrobble = () => { export const useScrobble = () => {
@@ -97,15 +98,15 @@ export const useScrobble = () => {
// const currentSong = current[0] as QueueSong | undefined; // const currentSong = current[0] as QueueSong | undefined;
const previousSong = previous[0] as QueueSong; const previousSong = previous[0] as QueueSong;
const previousSongTime = previous[1] as number; const previousSongTimeSec = previous[1] as number;
// Send completion scrobble when song changes and a previous song exists // Send completion scrobble when song changes and a previous song exists
if (previousSong?.id) { if (previousSong?.id) {
const shouldSubmitScrobble = checkScrobbleConditions({ const shouldSubmitScrobble = checkScrobbleConditions({
scrobbleAtDuration: scrobbleSettings?.scrobbleAtDuration, scrobbleAtDurationMs: (scrobbleSettings?.scrobbleAtDuration ?? 0) * 1000,
scrobbleAtPercentage: scrobbleSettings?.scrobbleAtPercentage, scrobbleAtPercentage: scrobbleSettings?.scrobbleAtPercentage,
songCompletedDuration: previousSongTime, songCompletedDurationMs: previousSongTimeSec * 1000,
songDuration: previousSong.duration, songDurationMs: previousSong.duration,
}); });
if ( if (
@@ -114,7 +115,7 @@ export const useScrobble = () => {
) { ) {
const position = const position =
previousSong?.serverType === ServerType.JELLYFIN previousSong?.serverType === ServerType.JELLYFIN
? previousSongTime * 1e7 ? previousSongTimeSec * 1e7
: undefined; : undefined;
sendScrobble.mutate({ sendScrobble.mutate({
@@ -168,7 +169,10 @@ export const useScrobble = () => {
); );
const handleScrobbleFromStatusChange = useCallback( const handleScrobbleFromStatusChange = useCallback(
(status: PlayerStatus | undefined) => { (
current: (PlayerStatus | number | undefined)[],
previous: (PlayerStatus | number | undefined)[],
) => {
if (!isScrobbleEnabled) return; if (!isScrobbleEnabled) return;
const currentSong = usePlayerStore.getState().current.song; const currentSong = usePlayerStore.getState().current.song;
@@ -180,8 +184,11 @@ export const useScrobble = () => {
? usePlayerStore.getState().current.time * 1e7 ? usePlayerStore.getState().current.time * 1e7
: undefined; : undefined;
const currentStatus = current[0] as PlayerStatus;
const currentTimeSec = current[1] as number;
// Whenever the player is restarted, send a 'start' scrobble // Whenever the player is restarted, send a 'start' scrobble
if (status === PlayerStatus.PLAYING) { if (currentStatus === PlayerStatus.PLAYING) {
sendScrobble.mutate({ sendScrobble.mutate({
query: { query: {
event: 'unpause', event: 'unpause',
@@ -194,7 +201,7 @@ export const useScrobble = () => {
if (currentSong?.serverType === ServerType.JELLYFIN) { if (currentSong?.serverType === ServerType.JELLYFIN) {
progressIntervalId.current = setInterval(() => { progressIntervalId.current = setInterval(() => {
const currentTime = usePlayerStore.getState().current.time; const currentTime = currentTimeSec;
handleScrobbleFromSeek(currentTime); handleScrobbleFromSeek(currentTime);
}, 10000); }, 10000);
} }
@@ -215,12 +222,17 @@ export const useScrobble = () => {
clearInterval(progressIntervalId.current as ReturnType<typeof setInterval>); clearInterval(progressIntervalId.current as ReturnType<typeof setInterval>);
} }
} else { } else {
const isLastTrackInQueue = usePlayerStore.getState().actions.checkIsLastTrack();
const previousTimeSec = previous[1] as number;
// If not already scrobbled, send a 'submission' scrobble if conditions are met // If not already scrobbled, send a 'submission' scrobble if conditions are met
const shouldSubmitScrobble = checkScrobbleConditions({ const shouldSubmitScrobble = checkScrobbleConditions({
scrobbleAtDuration: scrobbleSettings?.scrobbleAtDuration, scrobbleAtDurationMs: (scrobbleSettings?.scrobbleAtDuration ?? 0) * 1000,
scrobbleAtPercentage: scrobbleSettings?.scrobbleAtPercentage, scrobbleAtPercentage: scrobbleSettings?.scrobbleAtPercentage,
songCompletedDuration: usePlayerStore.getState().current.time, // If scrobbling the last song in the queue, use the previous time instead of the current time since otherwise time value will be 0
songDuration: currentSong.duration, songCompletedDurationMs:
(isLastTrackInQueue ? previousTimeSec : currentTimeSec) * 1000,
songDurationMs: currentSong.duration,
}); });
if (!isCurrentSongScrobbled && shouldSubmitScrobble) { if (!isCurrentSongScrobbled && shouldSubmitScrobble) {
@@ -261,10 +273,10 @@ export const useScrobble = () => {
currentSong?.serverType === ServerType.JELLYFIN ? currentTime * 1e7 : undefined; currentSong?.serverType === ServerType.JELLYFIN ? currentTime * 1e7 : undefined;
const shouldSubmitScrobble = checkScrobbleConditions({ const shouldSubmitScrobble = checkScrobbleConditions({
scrobbleAtDuration: scrobbleSettings?.scrobbleAtDuration, scrobbleAtDurationMs: (scrobbleSettings?.scrobbleAtDuration ?? 0) * 1000,
scrobbleAtPercentage: scrobbleSettings?.scrobbleAtPercentage, scrobbleAtPercentage: scrobbleSettings?.scrobbleAtPercentage,
songCompletedDuration: currentTime, songCompletedDurationMs: currentTime,
songDuration: currentSong.duration, songDurationMs: currentSong.duration,
}); });
if (!isCurrentSongScrobbled && shouldSubmitScrobble) { if (!isCurrentSongScrobbled && shouldSubmitScrobble) {
@@ -313,8 +325,11 @@ export const useScrobble = () => {
); );
const unsubStatusChange = usePlayerStore.subscribe( const unsubStatusChange = usePlayerStore.subscribe(
(state) => state.current.status, (state) => [state.current.status, state.current.time],
handleScrobbleFromStatusChange, handleScrobbleFromStatusChange,
{
equalityFn: (a, b) => (a[0] as PlayerStatus) === (b[0] as PlayerStatus),
},
); );
return () => { return () => {
@@ -7,23 +7,23 @@ interface AnimatedPageProps {
children: ReactNode; children: ReactNode;
} }
const variants = { // const variants = {
animate: { opacity: 1 }, // animate: { opacity: 1 },
exit: { opacity: 0 }, // exit: { opacity: 0 },
initial: { opacity: 0 }, // initial: { opacity: 0 },
}; // };
export const AnimatedPage = forwardRef( export const AnimatedPage = forwardRef(
({ children }: AnimatedPageProps, ref: Ref<HTMLDivElement>) => { ({ children }: AnimatedPageProps, ref: Ref<HTMLDivElement>) => {
return ( return (
<motion.main <motion.main
ref={ref} ref={ref}
animate="animate" // animate="animate"
className={styles.animatedPage} className={styles.animatedPage}
exit="exit" // exit="exit"
initial="initial" // initial="initial"
transition={{ duration: 0.3, ease: 'easeIn' }} // transition={{ duration: 0.3, ease: 'easeIn' }}
variants={variants} // variants={variants}
> >
{children} {children}
</motion.main> </motion.main>
@@ -29,7 +29,7 @@ export const SongListHeader = ({ gridRef, title, itemCount, tableRef }: SongList
const { display, filter } = useListStoreByKey({ key: pageKey }); const { display, filter } = useListStoreByKey({ key: pageKey });
const cq = useContainerQuery(); const cq = useContainerQuery();
const { handleRefreshTable } = useListFilterRefresh({ const { handleRefreshTable, handleRefreshGrid } = useListFilterRefresh({
itemType: LibraryItem.SONG, itemType: LibraryItem.SONG,
server, server,
}); });
@@ -51,7 +51,7 @@ export const SongListHeader = ({ gridRef, title, itemCount, tableRef }: SongList
handleRefreshTable(tableRef, filterWithCustom); handleRefreshTable(tableRef, filterWithCustom);
setTablePagination({ data: { currentPage: 0 }, key: pageKey }); setTablePagination({ data: { currentPage: 0 }, key: pageKey });
} else { } else {
// handleRefreshGrid(gridRef, filterWithCustom); handleRefreshGrid(gridRef, filterWithCustom);
} }
}, 500); }, 500);
@@ -91,7 +91,7 @@ export const AppMenu = () => {
}; };
const handleQuit = () => { const handleQuit = () => {
browser?.exit(); browser?.quit();
}; };
return ( return (
+5 -6
View File
@@ -1,10 +1,8 @@
import isElectron from 'is-electron';
import { lazy, Suspense } from 'react'; import { lazy, Suspense } from 'react';
import { import {
Route, Route,
createRoutesFromElements, createRoutesFromElements,
RouterProvider, RouterProvider,
createBrowserRouter,
createHashRouter, createHashRouter,
} from 'react-router-dom'; } from 'react-router-dom';
import { AppRoute } from './routes'; import { AppRoute } from './routes';
@@ -68,10 +66,8 @@ const RouteErrorBoundary = lazy(
() => import('/@/renderer/features/action-required/components/route-error-boundary'), () => import('/@/renderer/features/action-required/components/route-error-boundary'),
); );
const dynamicRouter = isElectron() ? createHashRouter : createBrowserRouter;
export const AppRouter = () => { export const AppRouter = () => {
const router = dynamicRouter( const router = createHashRouter(
createRoutesFromElements( createRoutesFromElements(
<> <>
<Route element={<TitlebarOutlet />}> <Route element={<TitlebarOutlet />}>
@@ -198,7 +194,10 @@ export const AppRouter = () => {
return ( return (
<Suspense fallback={<></>}> <Suspense fallback={<></>}>
<RouterProvider router={router} /> <RouterProvider
future={{ v7_startTransition: true }}
router={router}
/>
</Suspense> </Suspense>
); );
}; };
+6
View File
@@ -196,6 +196,7 @@ export interface SettingsSlice extends SettingsState {
reset: () => void; reset: () => void;
setSettings: (data: Partial<SettingsState>) => void; setSettings: (data: Partial<SettingsState>) => void;
setSidebarItems: (items: SidebarItemType[]) => void; setSidebarItems: (items: SidebarItemType[]) => void;
setTable: (type: TableType, data: DataTableProps) => void;
}; };
} }
@@ -493,6 +494,11 @@ export const useSettingsStore = create<SettingsSlice>()(
state.general.sidebarItems = items; state.general.sidebarItems = items;
}); });
}, },
setTable: (type: TableType, data: DataTableProps) => {
set((state) => {
state.tables[type] = data;
});
},
}, },
...initialState, ...initialState,
})), })),