mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-22 10:26:33 +02:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e62242824d | |||
| dbd82d46cf | |||
| e4378d1bc9 | |||
| f7ae4faeb0 | |||
| 749899cec0 | |||
| f9b9f9ab06 |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"version": "1.15.0",
|
"version": "1.15.1",
|
||||||
"description": "A modern self-hosted music player.",
|
"description": "A modern self-hosted music player.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"subsonic",
|
"subsonic",
|
||||||
|
|||||||
@@ -644,54 +644,6 @@ async function createWindow(first = true): Promise<void> {
|
|||||||
return mainWindow?.webContents.session.clearCache();
|
return mainWindow?.webContents.session.clearCache();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(
|
|
||||||
'app-check-for-updates',
|
|
||||||
async (): Promise<{ updateAvailable: boolean; version?: string }> => {
|
|
||||||
if (disableAutoUpdates()) {
|
|
||||||
console.log('Auto updates are disabled');
|
|
||||||
return { updateAvailable: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log('Checking for updates');
|
|
||||||
const effectiveChannel = store.get('release_channel') as string;
|
|
||||||
let result: null | UpdateCheckResult;
|
|
||||||
let updater: UpdaterInstance;
|
|
||||||
|
|
||||||
if (effectiveChannel === 'alpha') {
|
|
||||||
const best = await checkAllChannelsAndGetBest();
|
|
||||||
result = best.result;
|
|
||||||
updater = best.updater;
|
|
||||||
} else {
|
|
||||||
updater = configureAndGetUpdater();
|
|
||||||
result = await updater.checkForUpdates();
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateAvailable = result?.isUpdateAvailable ?? false;
|
|
||||||
console.log('Update available:', updateAvailable);
|
|
||||||
if (updateAvailable && store.get('disable_auto_updates') !== true) {
|
|
||||||
if (isMacOS()) {
|
|
||||||
getMainWindow()?.webContents.send(
|
|
||||||
'update-available',
|
|
||||||
result?.updateInfo?.version,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.log('Downloading update');
|
|
||||||
updater.downloadUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
updateAvailable,
|
|
||||||
version: result?.updateInfo?.version,
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
console.log('Error checking for updates');
|
|
||||||
return { updateAvailable: false };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
ipcMain.on('app-restart', () => {
|
ipcMain.on('app-restart', () => {
|
||||||
// Fix for .AppImage
|
// Fix for .AppImage
|
||||||
if (process.env.APPIMAGE) {
|
if (process.env.APPIMAGE) {
|
||||||
|
|||||||
@@ -11,3 +11,7 @@
|
|||||||
padding-left: var(--lyric-padding-left, 0%) !important;
|
padding-left: var(--lyric-padding-left, 0%) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export interface LyricsScrollContentProps {
|
|||||||
gap?: number;
|
gap?: number;
|
||||||
paddingLeft?: number;
|
paddingLeft?: number;
|
||||||
paddingRight?: number;
|
paddingRight?: number;
|
||||||
|
preview?: boolean;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,11 +21,19 @@ export const LyricsScrollContent = ({
|
|||||||
gap,
|
gap,
|
||||||
paddingLeft = 0,
|
paddingLeft = 0,
|
||||||
paddingRight = 0,
|
paddingRight = 0,
|
||||||
|
preview = false,
|
||||||
style,
|
style,
|
||||||
}: LyricsScrollContentProps) => {
|
}: LyricsScrollContentProps) => {
|
||||||
const contentStyle = useMemo(
|
const contentStyle = useMemo(() => {
|
||||||
() =>
|
if (preview) {
|
||||||
({
|
return {
|
||||||
|
gap: gap !== undefined ? `${gap}px` : undefined,
|
||||||
|
padding: 0,
|
||||||
|
...style,
|
||||||
|
} as React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
'--lyric-padding-left': `${paddingLeft}%`,
|
'--lyric-padding-left': `${paddingLeft}%`,
|
||||||
'--lyric-padding-right': `${paddingRight}%`,
|
'--lyric-padding-right': `${paddingRight}%`,
|
||||||
gap: gap !== undefined ? `${gap}px` : undefined,
|
gap: gap !== undefined ? `${gap}px` : undefined,
|
||||||
@@ -33,12 +42,14 @@ export const LyricsScrollContent = ({
|
|||||||
paddingRight: `${paddingRight}%`,
|
paddingRight: `${paddingRight}%`,
|
||||||
paddingTop: '10vh',
|
paddingTop: '10vh',
|
||||||
...style,
|
...style,
|
||||||
}) as React.CSSProperties,
|
} as React.CSSProperties;
|
||||||
[bottomScrollPadding, gap, paddingLeft, paddingRight, style],
|
}, [bottomScrollPadding, gap, paddingLeft, paddingRight, preview, style]);
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(styles.content, className)} style={contentStyle}>
|
<div
|
||||||
|
className={clsx(styles.content, preview && styles.preview, className)}
|
||||||
|
style={contentStyle}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,22 +33,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.lyrics-preview {
|
|
||||||
:global(.synchronized-lyrics) {
|
|
||||||
height: auto !important;
|
|
||||||
padding: 1rem 0 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
transform: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyrics-content-wrapper {
|
|
||||||
:global(> div) {
|
|
||||||
height: auto !important;
|
|
||||||
max-height: none !important;
|
|
||||||
padding: 1rem 0 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
transform: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -246,7 +246,6 @@ export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearch
|
|||||||
{selectedResult && (
|
{selectedResult && (
|
||||||
<Stack style={{ flex: 1, height: '100%', minHeight: 0, overflow: 'hidden' }}>
|
<Stack style={{ flex: 1, height: '100%', minHeight: 0, overflow: 'hidden' }}>
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
className={styles['lyrics-preview']}
|
|
||||||
style={{
|
style={{
|
||||||
height: '100%',
|
height: '100%',
|
||||||
paddingRight: '1rem',
|
paddingRight: '1rem',
|
||||||
@@ -255,14 +254,10 @@ export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearch
|
|||||||
{isPreviewLoading ? (
|
{isPreviewLoading ? (
|
||||||
<Spinner container />
|
<Spinner container />
|
||||||
) : previewData ? (
|
) : previewData ? (
|
||||||
<div
|
Array.isArray(previewData) ? (
|
||||||
className={styles['lyrics-content-wrapper']}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
>
|
|
||||||
{Array.isArray(previewData) ? (
|
|
||||||
lyricsHasWordCues(previewData) ? (
|
lyricsHasWordCues(previewData) ? (
|
||||||
<SynchronizedKaraokeLyrics
|
<SynchronizedKaraokeLyrics
|
||||||
style={{ padding: 0 }}
|
preview
|
||||||
{...({
|
{...({
|
||||||
artist: selectedResult.artist,
|
artist: selectedResult.artist,
|
||||||
lyrics: previewData,
|
lyrics: previewData,
|
||||||
@@ -273,7 +268,7 @@ export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearch
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<SynchronizedLyrics
|
<SynchronizedLyrics
|
||||||
style={{ padding: 0 }}
|
preview
|
||||||
{...({
|
{...({
|
||||||
artist: selectedResult.artist,
|
artist: selectedResult.artist,
|
||||||
lyrics: previewData,
|
lyrics: previewData,
|
||||||
@@ -285,6 +280,7 @@ export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearch
|
|||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<UnsynchronizedLyrics
|
<UnsynchronizedLyrics
|
||||||
|
preview
|
||||||
{...({
|
{...({
|
||||||
artist: selectedResult.artist,
|
artist: selectedResult.artist,
|
||||||
lyrics: previewData,
|
lyrics: previewData,
|
||||||
@@ -293,8 +289,7 @@ export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearch
|
|||||||
source: selectedResult.source,
|
source: selectedResult.source,
|
||||||
} as UnsynchronizedLyricsProps)}
|
} as UnsynchronizedLyricsProps)}
|
||||||
/>
|
/>
|
||||||
)}
|
)
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<Center>
|
<Center>
|
||||||
<Text isMuted>{t('page.fullscreenPlayer.noLyrics')}</Text>
|
<Text isMuted>{t('page.fullscreenPlayer.noLyrics')}</Text>
|
||||||
|
|||||||
@@ -7,3 +7,11 @@
|
|||||||
overflow: hidden auto;
|
overflow: hidden auto;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
height: auto;
|
||||||
|
max-height: none;
|
||||||
|
padding: 1rem 0;
|
||||||
|
overflow: visible;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export interface SynchronizedKaraokeLyricsProps extends Omit<FullLyricsMetadata,
|
|||||||
extraOverlayLyrics?: SynchronizedLyricsData[];
|
extraOverlayLyrics?: SynchronizedLyricsData[];
|
||||||
lyrics: SynchronizedLyricsData;
|
lyrics: SynchronizedLyricsData;
|
||||||
offsetMs?: number;
|
offsetMs?: number;
|
||||||
|
preview?: boolean;
|
||||||
pronunciationLyrics?: null | SynchronizedLyricsData;
|
pronunciationLyrics?: null | SynchronizedLyricsData;
|
||||||
romajiLyrics?: null | SynchronizedLyricsData;
|
romajiLyrics?: null | SynchronizedLyricsData;
|
||||||
settingsKey?: string;
|
settingsKey?: string;
|
||||||
@@ -44,6 +45,8 @@ export interface SynchronizedKaraokeLyricsProps extends Omit<FullLyricsMetadata,
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SEEK_DETECT_THRESHOLD_MS = 500;
|
const SEEK_DETECT_THRESHOLD_MS = 500;
|
||||||
|
const PREVIEW_FONT_SIZE = 20;
|
||||||
|
const PREVIEW_GAP = 20;
|
||||||
|
|
||||||
export const SynchronizedKaraokeLyrics = ({
|
export const SynchronizedKaraokeLyrics = ({
|
||||||
agents,
|
agents,
|
||||||
@@ -52,6 +55,7 @@ export const SynchronizedKaraokeLyrics = ({
|
|||||||
lyrics,
|
lyrics,
|
||||||
name,
|
name,
|
||||||
offsetMs,
|
offsetMs,
|
||||||
|
preview = false,
|
||||||
pronunciationLyrics,
|
pronunciationLyrics,
|
||||||
romajiLyrics,
|
romajiLyrics,
|
||||||
settingsKey = 'default',
|
settingsKey = 'default',
|
||||||
@@ -77,6 +81,11 @@ export const SynchronizedKaraokeLyrics = ({
|
|||||||
showScrollbar,
|
showScrollbar,
|
||||||
} = useSynchronizedLyricsBase(settingsKey, offsetMs);
|
} = useSynchronizedLyricsBase(settingsKey, offsetMs);
|
||||||
|
|
||||||
|
const effectiveFontSize = preview ? PREVIEW_FONT_SIZE : settings.fontSize;
|
||||||
|
const effectiveGap = preview ? PREVIEW_GAP : settings.gap;
|
||||||
|
const effectivePaddingLeft = preview ? 0 : settings.paddingLeft;
|
||||||
|
const effectivePaddingRight = preview ? 0 : settings.paddingRight;
|
||||||
|
|
||||||
const normalizedLyrics = useMemo(() => normalizeLyrics(lyrics), [lyrics]);
|
const normalizedLyrics = useMemo(() => normalizeLyrics(lyrics), [lyrics]);
|
||||||
const rafRef = useRef<null | number>(null);
|
const rafRef = useRef<null | number>(null);
|
||||||
const statusRef = useRef(usePlayerStoreBase.getState().player.status);
|
const statusRef = useRef(usePlayerStoreBase.getState().player.status);
|
||||||
@@ -97,13 +106,13 @@ export const SynchronizedKaraokeLyrics = ({
|
|||||||
containerRef,
|
containerRef,
|
||||||
followRef,
|
followRef,
|
||||||
followScrollAlignmentRef,
|
followScrollAlignmentRef,
|
||||||
fontSize: settings.fontSize,
|
fontSize: effectiveFontSize,
|
||||||
gap: settings.gap,
|
gap: effectiveGap,
|
||||||
lineIdPrefix: 'karaoke-line',
|
lineIdPrefix: 'karaoke-line',
|
||||||
lineLeadTimeMsRef,
|
lineLeadTimeMsRef,
|
||||||
lyrics: normalizedLyrics,
|
lyrics: normalizedLyrics,
|
||||||
paddingLeft: settings.paddingLeft,
|
paddingLeft: effectivePaddingLeft,
|
||||||
paddingRight: settings.paddingRight,
|
paddingRight: effectivePaddingRight,
|
||||||
scrollContainerId: LYRICS_SCROLL_CONTAINER_ID,
|
scrollContainerId: LYRICS_SCROLL_CONTAINER_ID,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -328,7 +337,11 @@ export const SynchronizedKaraokeLyrics = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(styles.container, 'synchronized-karaoke-lyrics overlay-scrollbar')}
|
className={clsx(
|
||||||
|
styles.container,
|
||||||
|
preview && styles.preview,
|
||||||
|
'synchronized-karaoke-lyrics overlay-scrollbar',
|
||||||
|
)}
|
||||||
id={LYRICS_SCROLL_CONTAINER_ID}
|
id={LYRICS_SCROLL_CONTAINER_ID}
|
||||||
onClick={handleContainerClick}
|
onClick={handleContainerClick}
|
||||||
onMouseEnter={showScrollbar}
|
onMouseEnter={showScrollbar}
|
||||||
@@ -337,15 +350,16 @@ export const SynchronizedKaraokeLyrics = ({
|
|||||||
style={{ ...containerStyle, ...style }}
|
style={{ ...containerStyle, ...style }}
|
||||||
>
|
>
|
||||||
<LyricsScrollContent
|
<LyricsScrollContent
|
||||||
gap={settings.gap}
|
gap={effectiveGap}
|
||||||
paddingLeft={settings.paddingLeft}
|
paddingLeft={effectivePaddingLeft}
|
||||||
paddingRight={settings.paddingRight}
|
paddingRight={effectivePaddingRight}
|
||||||
|
preview={preview}
|
||||||
>
|
>
|
||||||
{settings.showProvider && source && (
|
{settings.showProvider && source && (
|
||||||
<LyricLine
|
<LyricLine
|
||||||
alignment={settings.alignment}
|
alignment={settings.alignment}
|
||||||
className="lyric-credit"
|
className="lyric-credit"
|
||||||
fontSize={settings.fontSize}
|
fontSize={effectiveFontSize}
|
||||||
text={`${source}`}
|
text={`${source}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -353,7 +367,7 @@ export const SynchronizedKaraokeLyrics = ({
|
|||||||
<LyricLine
|
<LyricLine
|
||||||
alignment={settings.alignment}
|
alignment={settings.alignment}
|
||||||
className="lyric-credit"
|
className="lyric-credit"
|
||||||
fontSize={settings.fontSize}
|
fontSize={effectiveFontSize}
|
||||||
text={`${name} — ${artist}`}
|
text={`${name} — ${artist}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -396,7 +410,7 @@ export const SynchronizedKaraokeLyrics = ({
|
|||||||
alignment={settings.alignment}
|
alignment={settings.alignment}
|
||||||
className="lyric-line synchronized"
|
className="lyric-line synchronized"
|
||||||
data-lyric-time={lineStartMs}
|
data-lyric-time={lineStartMs}
|
||||||
fontSize={settings.fontSize}
|
fontSize={effectiveFontSize}
|
||||||
id={`karaoke-line-${idx}`}
|
id={`karaoke-line-${idx}`}
|
||||||
key={idx}
|
key={idx}
|
||||||
romajiText={pronunciationText}
|
romajiText={pronunciationText}
|
||||||
@@ -414,7 +428,7 @@ export const SynchronizedKaraokeLyrics = ({
|
|||||||
cueLines={rawLine.cueLines}
|
cueLines={rawLine.cueLines}
|
||||||
data-lyric-time={lineStartMs}
|
data-lyric-time={lineStartMs}
|
||||||
extraOverlays={extraOverlays}
|
extraOverlays={extraOverlays}
|
||||||
fontSize={settings.fontSize}
|
fontSize={effectiveFontSize}
|
||||||
id={`karaoke-line-${idx}`}
|
id={`karaoke-line-${idx}`}
|
||||||
key={idx}
|
key={idx}
|
||||||
lineIndex={idx}
|
lineIndex={idx}
|
||||||
|
|||||||
@@ -8,3 +8,11 @@
|
|||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
transform: translateY(-2rem);
|
transform: translateY(-2rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
height: auto;
|
||||||
|
max-height: none;
|
||||||
|
padding: 1rem 0;
|
||||||
|
overflow: visible;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export interface SynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyric
|
|||||||
extraOverlayLyrics?: SynchronizedLyricsData[];
|
extraOverlayLyrics?: SynchronizedLyricsData[];
|
||||||
lyrics: SynchronizedLyricsData;
|
lyrics: SynchronizedLyricsData;
|
||||||
offsetMs?: number;
|
offsetMs?: number;
|
||||||
|
preview?: boolean;
|
||||||
pronunciationLyrics?: null | SynchronizedLyricsData;
|
pronunciationLyrics?: null | SynchronizedLyricsData;
|
||||||
romajiLyrics?: null | SynchronizedLyricsData;
|
romajiLyrics?: null | SynchronizedLyricsData;
|
||||||
settingsKey?: string;
|
settingsKey?: string;
|
||||||
@@ -38,12 +39,15 @@ export interface SynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyric
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SEEK_DETECT_THRESHOLD_MS = 500;
|
const SEEK_DETECT_THRESHOLD_MS = 500;
|
||||||
|
const PREVIEW_FONT_SIZE = 20;
|
||||||
|
const PREVIEW_GAP = 20;
|
||||||
|
|
||||||
export const SynchronizedLyrics = ({
|
export const SynchronizedLyrics = ({
|
||||||
artist,
|
artist,
|
||||||
lyrics,
|
lyrics,
|
||||||
name,
|
name,
|
||||||
offsetMs,
|
offsetMs,
|
||||||
|
preview = false,
|
||||||
pronunciationLyrics,
|
pronunciationLyrics,
|
||||||
romajiLyrics,
|
romajiLyrics,
|
||||||
settingsKey = 'default',
|
settingsKey = 'default',
|
||||||
@@ -68,6 +72,11 @@ export const SynchronizedLyrics = ({
|
|||||||
showScrollbar,
|
showScrollbar,
|
||||||
} = useSynchronizedLyricsBase(settingsKey, offsetMs);
|
} = useSynchronizedLyricsBase(settingsKey, offsetMs);
|
||||||
|
|
||||||
|
const effectiveFontSize = preview ? PREVIEW_FONT_SIZE : settings.fontSize;
|
||||||
|
const effectiveGap = preview ? PREVIEW_GAP : settings.gap;
|
||||||
|
const effectivePaddingLeft = preview ? 0 : settings.paddingLeft;
|
||||||
|
const effectivePaddingRight = preview ? 0 : settings.paddingRight;
|
||||||
|
|
||||||
const normalizedLyrics = useMemo(() => normalizeLyrics(lyrics), [lyrics]);
|
const normalizedLyrics = useMemo(() => normalizeLyrics(lyrics), [lyrics]);
|
||||||
const rafRef = useRef<null | number>(null);
|
const rafRef = useRef<null | number>(null);
|
||||||
const statusRef = useRef(usePlayerStoreBase.getState().player.status);
|
const statusRef = useRef(usePlayerStoreBase.getState().player.status);
|
||||||
@@ -83,13 +92,13 @@ export const SynchronizedLyrics = ({
|
|||||||
containerRef,
|
containerRef,
|
||||||
followRef,
|
followRef,
|
||||||
followScrollAlignmentRef,
|
followScrollAlignmentRef,
|
||||||
fontSize: settings.fontSize,
|
fontSize: effectiveFontSize,
|
||||||
gap: settings.gap,
|
gap: effectiveGap,
|
||||||
lineIdPrefix: 'lyric',
|
lineIdPrefix: 'lyric',
|
||||||
lineLeadTimeMsRef,
|
lineLeadTimeMsRef,
|
||||||
lyrics: normalizedLyrics,
|
lyrics: normalizedLyrics,
|
||||||
paddingLeft: settings.paddingLeft,
|
paddingLeft: effectivePaddingLeft,
|
||||||
paddingRight: settings.paddingRight,
|
paddingRight: effectivePaddingRight,
|
||||||
scrollContainerId: LYRICS_SCROLL_CONTAINER_ID,
|
scrollContainerId: LYRICS_SCROLL_CONTAINER_ID,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -240,7 +249,11 @@ export const SynchronizedLyrics = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(styles.container, 'synchronized-lyrics overlay-scrollbar')}
|
className={clsx(
|
||||||
|
styles.container,
|
||||||
|
preview && styles.preview,
|
||||||
|
'synchronized-lyrics overlay-scrollbar',
|
||||||
|
)}
|
||||||
id={LYRICS_SCROLL_CONTAINER_ID}
|
id={LYRICS_SCROLL_CONTAINER_ID}
|
||||||
onClick={handleContainerClick}
|
onClick={handleContainerClick}
|
||||||
onMouseEnter={showScrollbar}
|
onMouseEnter={showScrollbar}
|
||||||
@@ -249,15 +262,16 @@ export const SynchronizedLyrics = ({
|
|||||||
style={{ ...containerStyle, ...style }}
|
style={{ ...containerStyle, ...style }}
|
||||||
>
|
>
|
||||||
<LyricsScrollContent
|
<LyricsScrollContent
|
||||||
gap={settings.gap}
|
gap={effectiveGap}
|
||||||
paddingLeft={settings.paddingLeft}
|
paddingLeft={effectivePaddingLeft}
|
||||||
paddingRight={settings.paddingRight}
|
paddingRight={effectivePaddingRight}
|
||||||
|
preview={preview}
|
||||||
>
|
>
|
||||||
{settings.showProvider && source && (
|
{settings.showProvider && source && (
|
||||||
<LyricLine
|
<LyricLine
|
||||||
alignment={settings.alignment}
|
alignment={settings.alignment}
|
||||||
className="lyric-credit"
|
className="lyric-credit"
|
||||||
fontSize={settings.fontSize}
|
fontSize={effectiveFontSize}
|
||||||
text={`${source}`}
|
text={`${source}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -265,7 +279,7 @@ export const SynchronizedLyrics = ({
|
|||||||
<LyricLine
|
<LyricLine
|
||||||
alignment={settings.alignment}
|
alignment={settings.alignment}
|
||||||
className="lyric-credit"
|
className="lyric-credit"
|
||||||
fontSize={settings.fontSize}
|
fontSize={effectiveFontSize}
|
||||||
text={`${name} — ${artist}`}
|
text={`${name} — ${artist}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -290,7 +304,7 @@ export const SynchronizedLyrics = ({
|
|||||||
alignment={settings.alignment}
|
alignment={settings.alignment}
|
||||||
className="lyric-line synchronized"
|
className="lyric-line synchronized"
|
||||||
data-lyric-time={lineStartMs}
|
data-lyric-time={lineStartMs}
|
||||||
fontSize={settings.fontSize}
|
fontSize={effectiveFontSize}
|
||||||
id={`lyric-${idx}`}
|
id={`lyric-${idx}`}
|
||||||
key={idx}
|
key={idx}
|
||||||
romajiText={pronunciationText}
|
romajiText={pronunciationText}
|
||||||
|
|||||||
@@ -7,3 +7,11 @@
|
|||||||
overflow: hidden auto;
|
overflow: hidden auto;
|
||||||
transform: translateY(-2rem);
|
transform: translateY(-2rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
height: auto;
|
||||||
|
max-height: none;
|
||||||
|
padding: 1rem 0;
|
||||||
|
overflow: visible;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import styles from './unsynchronized-lyrics.module.css';
|
import styles from './unsynchronized-lyrics.module.css';
|
||||||
@@ -9,15 +10,20 @@ import { FullLyricsMetadata } from '/@/shared/types/domain-types';
|
|||||||
|
|
||||||
export interface UnsynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
|
export interface UnsynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
|
||||||
lyrics: string;
|
lyrics: string;
|
||||||
|
preview?: boolean;
|
||||||
romajiLyrics?: null | string;
|
romajiLyrics?: null | string;
|
||||||
settingsKey?: string;
|
settingsKey?: string;
|
||||||
translatedLyrics?: null | string;
|
translatedLyrics?: null | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PREVIEW_FONT_SIZE = 20;
|
||||||
|
const PREVIEW_GAP = 20;
|
||||||
|
|
||||||
export const UnsynchronizedLyrics = ({
|
export const UnsynchronizedLyrics = ({
|
||||||
artist,
|
artist,
|
||||||
lyrics,
|
lyrics,
|
||||||
name,
|
name,
|
||||||
|
preview = false,
|
||||||
romajiLyrics,
|
romajiLyrics,
|
||||||
settingsKey = 'default',
|
settingsKey = 'default',
|
||||||
source,
|
source,
|
||||||
@@ -27,12 +33,14 @@ export const UnsynchronizedLyrics = ({
|
|||||||
const displaySettings = useLyricsDisplaySettings(settingsKey);
|
const displaySettings = useLyricsDisplaySettings(settingsKey);
|
||||||
const settings = {
|
const settings = {
|
||||||
...lyricsSettings,
|
...lyricsSettings,
|
||||||
fontSizeUnsync:
|
fontSizeUnsync: preview
|
||||||
displaySettings.fontSizeUnsync && displaySettings.fontSizeUnsync !== 0
|
? PREVIEW_FONT_SIZE
|
||||||
|
: displaySettings.fontSizeUnsync && displaySettings.fontSizeUnsync !== 0
|
||||||
? displaySettings.fontSizeUnsync
|
? displaySettings.fontSizeUnsync
|
||||||
: 24,
|
: 24,
|
||||||
gapUnsync:
|
gapUnsync: preview
|
||||||
displaySettings.gapUnsync && displaySettings.gapUnsync !== 0
|
? PREVIEW_GAP
|
||||||
|
: displaySettings.gapUnsync && displaySettings.gapUnsync !== 0
|
||||||
? displaySettings.gapUnsync
|
? displaySettings.gapUnsync
|
||||||
: 24,
|
: 24,
|
||||||
};
|
};
|
||||||
@@ -49,12 +57,13 @@ export const UnsynchronizedLyrics = ({
|
|||||||
}, [romajiLyrics]);
|
}, [romajiLyrics]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={clsx(styles.container, preview && styles.preview)}>
|
||||||
<LyricsScrollContent
|
<LyricsScrollContent
|
||||||
bottomScrollPadding="6vh"
|
bottomScrollPadding="6vh"
|
||||||
gap={settings.gapUnsync}
|
gap={settings.gapUnsync}
|
||||||
paddingLeft={displaySettings.paddingLeft ?? 0}
|
paddingLeft={preview ? 0 : (displaySettings.paddingLeft ?? 0)}
|
||||||
paddingRight={displaySettings.paddingRight ?? 0}
|
paddingRight={preview ? 0 : (displaySettings.paddingRight ?? 0)}
|
||||||
|
preview={preview}
|
||||||
>
|
>
|
||||||
{settings.showProvider && source && (
|
{settings.showProvider && source && (
|
||||||
<LyricLine
|
<LyricLine
|
||||||
|
|||||||
@@ -185,9 +185,9 @@ export const useScrobble = () => {
|
|||||||
// Because Jellyfin uses the stop event for submission, we need to send another
|
// Because Jellyfin uses the stop event for submission, we need to send another
|
||||||
// progress update after submission so that the song continues to progress in the dashboard
|
// progress update after submission so that the song continues to progress in the dashboard
|
||||||
const shouldSendProgress =
|
const shouldSendProgress =
|
||||||
song._serverType !== ServerType.JELLYFIN ||
|
song._serverType === ServerType.JELLYFIN &&
|
||||||
activeSong?._uniqueId !== song._uniqueId ||
|
activeSong?._uniqueId === song._uniqueId &&
|
||||||
status !== PlayerStatus.PLAYING;
|
status === PlayerStatus.PLAYING;
|
||||||
|
|
||||||
if (!shouldSendProgress) {
|
if (!shouldSendProgress) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { closeAllModals, openModal } from '@mantine/modals';
|
import { closeAllModals, openModal } from '@mantine/modals';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { UpdateAvailableButton } from '/@/renderer/features/settings/components/update-available-button';
|
||||||
import { useSettingSearchContext } from '/@/renderer/features/settings/context/search-context';
|
import { useSettingSearchContext } from '/@/renderer/features/settings/context/search-context';
|
||||||
import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar';
|
import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar';
|
||||||
import { SearchInput } from '/@/renderer/features/shared/components/search-input';
|
import { SearchInput } from '/@/renderer/features/shared/components/search-input';
|
||||||
@@ -14,9 +15,10 @@ import { Text } from '/@/shared/components/text/text';
|
|||||||
|
|
||||||
export type SettingsHeaderProps = {
|
export type SettingsHeaderProps = {
|
||||||
setSearch: (search: string) => void;
|
setSearch: (search: string) => void;
|
||||||
|
showUpdateAvailable?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SettingsHeader = ({ setSearch }: SettingsHeaderProps) => {
|
export const SettingsHeader = ({ setSearch, showUpdateAvailable }: SettingsHeaderProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { reset } = useSettingsStoreActions();
|
const { reset } = useSettingsStoreActions();
|
||||||
const search = useSettingSearchContext();
|
const search = useSettingSearchContext();
|
||||||
@@ -48,6 +50,7 @@ export const SettingsHeader = ({ setSearch }: SettingsHeaderProps) => {
|
|||||||
</LibraryHeaderBar.Title>
|
</LibraryHeaderBar.Title>
|
||||||
</Group>
|
</Group>
|
||||||
<Group>
|
<Group>
|
||||||
|
{showUpdateAvailable && <UpdateAvailableButton />}
|
||||||
<SearchInput
|
<SearchInput
|
||||||
defaultValue={search}
|
defaultValue={search}
|
||||||
onChange={(event) => setSearch(event.target.value.toLocaleLowerCase())}
|
onChange={(event) => setSearch(event.target.value.toLocaleLowerCase())}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const SettingsContextModal = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingSearchContext.Provider value={search}>
|
<SettingSearchContext.Provider value={search}>
|
||||||
<SettingsHeader setSearch={setSearch} />
|
<SettingsHeader setSearch={setSearch} showUpdateAvailable />
|
||||||
<SettingsContent />
|
<SettingsContent />
|
||||||
</SettingSearchContext.Provider>
|
</SettingSearchContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { toTag } from '/@/renderer/hooks';
|
||||||
|
import { useLatestVersion } from '/@/renderer/store';
|
||||||
|
import { Button } from '/@/shared/components/button/button';
|
||||||
|
|
||||||
|
export const UpdateAvailableButton = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { currentVersion, isUpdateAvailable, latestVersion } = useLatestVersion();
|
||||||
|
|
||||||
|
if (!isUpdateAvailable || !latestVersion) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
component="a"
|
||||||
|
href={`https://github.com/jeffvli/feishin/releases/tag/${toTag(latestVersion || currentVersion)}`}
|
||||||
|
size="compact-sm"
|
||||||
|
target="_blank"
|
||||||
|
variant="filled"
|
||||||
|
>
|
||||||
|
{t('common.newVersionAvailable')}: v{latestVersion}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -4,11 +4,11 @@ import { Fragment, ReactNode } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Link, useNavigate } from 'react-router';
|
import { Link, useNavigate } from 'react-router';
|
||||||
|
|
||||||
import packageJson from '../../../../../package.json';
|
|
||||||
import styles from './app-menu.module.css';
|
import styles from './app-menu.module.css';
|
||||||
|
|
||||||
import { isServerLock } from '/@/renderer/features/action-required/utils/window-properties';
|
import { isServerLock } from '/@/renderer/features/action-required/utils/window-properties';
|
||||||
import { ServerList } from '/@/renderer/features/servers/components/server-list';
|
import { ServerList } from '/@/renderer/features/servers/components/server-list';
|
||||||
|
import { UpdateAvailableButton } from '/@/renderer/features/settings/components/update-available-button';
|
||||||
import { openSettingsModal } from '/@/renderer/features/settings/utils/open-settings-modal';
|
import { openSettingsModal } from '/@/renderer/features/settings/utils/open-settings-modal';
|
||||||
import { ServerSelector } from '/@/renderer/features/sidebar/components/server-selector';
|
import { ServerSelector } from '/@/renderer/features/sidebar/components/server-selector';
|
||||||
import { openReleaseNotesModal } from '/@/renderer/release-notes-modal';
|
import { openReleaseNotesModal } from '/@/renderer/release-notes-modal';
|
||||||
@@ -18,10 +18,12 @@ import {
|
|||||||
useCommandPalette,
|
useCommandPalette,
|
||||||
useCurrentServer,
|
useCurrentServer,
|
||||||
useGeneralSettings,
|
useGeneralSettings,
|
||||||
|
useLatestVersion,
|
||||||
useSettingsStoreActions,
|
useSettingsStoreActions,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
import { DropdownMenu, MenuItemProps } from '/@/shared/components/dropdown-menu/dropdown-menu';
|
import { DropdownMenu, MenuItemProps } from '/@/shared/components/dropdown-menu/dropdown-menu';
|
||||||
|
import { Flex } from '/@/shared/components/flex/flex';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
import { toast } from '/@/shared/components/toast/toast';
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
@@ -138,6 +140,8 @@ export const AppMenu = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { currentVersion } = useLatestVersion();
|
||||||
|
|
||||||
const serverHeaderMenuItems: MenuItem[] = currentServer
|
const serverHeaderMenuItems: MenuItem[] = currentServer
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@@ -270,10 +274,10 @@ export const AppMenu = () => {
|
|||||||
{
|
{
|
||||||
icon: 'brandGitHub',
|
icon: 'brandGitHub',
|
||||||
id: 'version',
|
id: 'version',
|
||||||
label: t('page.appMenu.version', { version: packageJson.version }),
|
label: t('page.appMenu.version', { version: currentVersion }),
|
||||||
onClick: () =>
|
onClick: () =>
|
||||||
openReleaseNotesModal(
|
openReleaseNotesModal(
|
||||||
t('common.newVersion', { version: packageJson.version }) as string,
|
t('common.newVersion', { version: currentVersion }) as string,
|
||||||
),
|
),
|
||||||
type: 'item',
|
type: 'item',
|
||||||
},
|
},
|
||||||
@@ -301,6 +305,15 @@ export const AppMenu = () => {
|
|||||||
},
|
},
|
||||||
type: 'conditional-item',
|
type: 'conditional-item',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: (
|
||||||
|
<Flex align="center" justify="center" w="100%">
|
||||||
|
<UpdateAvailableButton />
|
||||||
|
</Flex>
|
||||||
|
),
|
||||||
|
id: 'update-available',
|
||||||
|
type: 'custom',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'divider-5',
|
id: 'divider-5',
|
||||||
type: 'divider',
|
type: 'divider',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export * from './use-app-focus';
|
|||||||
export * from './use-check-for-updates';
|
export * from './use-check-for-updates';
|
||||||
export * from './use-container-query';
|
export * from './use-container-query';
|
||||||
export * from './use-fast-average-color';
|
export * from './use-fast-average-color';
|
||||||
|
export * from './use-github-releases';
|
||||||
export * from './use-hide-scrollbar';
|
export * from './use-hide-scrollbar';
|
||||||
export * from './use-hotkeys';
|
export * from './use-hotkeys';
|
||||||
export * from './use-is-mounted';
|
export * from './use-is-mounted';
|
||||||
|
|||||||
@@ -1,29 +1,24 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useEffect } from 'react';
|
||||||
import isElectron from 'is-electron';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
|
import { parseVersionFromTag, useGithubLatestRelease } from '/@/renderer/hooks/use-github-releases';
|
||||||
|
import { useAppStore } from '/@/renderer/store';
|
||||||
|
|
||||||
|
// 6 hours
|
||||||
const CHECK_FOR_UPDATES_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
const CHECK_FOR_UPDATES_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
||||||
|
|
||||||
const utils = isElectron() ? window.api?.utils : null;
|
|
||||||
|
|
||||||
export const useCheckForUpdates = () => {
|
export const useCheckForUpdates = () => {
|
||||||
const [enablePeriodicCheck, setEnablePeriodicCheck] = useState(false);
|
const setLatestVersion = useAppStore((state) => state.actions.setLatestVersion);
|
||||||
|
|
||||||
// We want to skip the first check since it's already checked in the main process when the app is started
|
const query = useGithubLatestRelease({
|
||||||
useEffect(() => {
|
|
||||||
const timer = setTimeout(() => setEnablePeriodicCheck(true), CHECK_FOR_UPDATES_INTERVAL_MS);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const isEnabled =
|
|
||||||
enablePeriodicCheck &&
|
|
||||||
Boolean(isElectron() && utils?.checkForUpdates && !utils?.disableAutoUpdates?.());
|
|
||||||
|
|
||||||
return useQuery({
|
|
||||||
enabled: isEnabled,
|
|
||||||
queryFn: () => utils?.checkForUpdates?.(),
|
|
||||||
queryKey: ['app-check-for-updates'],
|
|
||||||
refetchInterval: CHECK_FOR_UPDATES_INTERVAL_MS,
|
refetchInterval: CHECK_FOR_UPDATES_INTERVAL_MS,
|
||||||
refetchIntervalInBackground: true,
|
refetchIntervalInBackground: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (query.data) {
|
||||||
|
setLatestVersion(parseVersionFromTag(query.data.tag_name));
|
||||||
|
}
|
||||||
|
}, [query.data, setLatestVersion]);
|
||||||
|
|
||||||
|
return query;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export const GITHUB_RELEASES_URL = 'https://api.github.com/repos/jeffvli/feishin/releases';
|
||||||
|
export const RELEASES_TO_FETCH = 30;
|
||||||
|
|
||||||
|
export interface GitHubRelease {
|
||||||
|
body: null | string;
|
||||||
|
name: null | string;
|
||||||
|
prerelease: boolean;
|
||||||
|
published_at: string;
|
||||||
|
tag_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseVersionFromTag(tagName: string): string {
|
||||||
|
return tagName.startsWith('v') ? tagName.slice(1) : tagName;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toTag(version: string): string {
|
||||||
|
return version.startsWith('v') ? version : `v${version}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGithubReleasesList = (perPage = RELEASES_TO_FETCH) => {
|
||||||
|
return useQuery({
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await axios.get<GitHubRelease[]>(GITHUB_RELEASES_URL, {
|
||||||
|
params: { per_page: perPage },
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
queryKey: ['github-releases-list', perPage],
|
||||||
|
retry: 2,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGithubLatestRelease = (options?: {
|
||||||
|
refetchInterval?: number;
|
||||||
|
refetchIntervalInBackground?: boolean;
|
||||||
|
}) => {
|
||||||
|
return useQuery({
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await axios.get<GitHubRelease>(`${GITHUB_RELEASES_URL}/latest`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
queryKey: ['github-latest-release'],
|
||||||
|
retry: 2,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -7,6 +7,14 @@ import { useTranslation } from 'react-i18next';
|
|||||||
|
|
||||||
import packageJson from '../../package.json';
|
import packageJson from '../../package.json';
|
||||||
|
|
||||||
|
import {
|
||||||
|
GITHUB_RELEASES_URL,
|
||||||
|
type GitHubRelease,
|
||||||
|
parseVersionFromTag,
|
||||||
|
RELEASES_TO_FETCH,
|
||||||
|
toTag,
|
||||||
|
useGithubReleasesList,
|
||||||
|
} from '/@/renderer/hooks/use-github-releases';
|
||||||
import { formatHrDateTime } from '/@/renderer/utils/format';
|
import { formatHrDateTime } from '/@/renderer/utils/format';
|
||||||
import { Button } from '/@/shared/components/button/button';
|
import { Button } from '/@/shared/components/button/button';
|
||||||
import { Center } from '/@/shared/components/center/center';
|
import { Center } from '/@/shared/components/center/center';
|
||||||
@@ -19,9 +27,7 @@ import { Stack } from '/@/shared/components/stack/stack';
|
|||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
|
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
|
||||||
|
|
||||||
const GITHUB_RELEASES_URL = 'https://api.github.com/repos/jeffvli/feishin/releases';
|
|
||||||
const GITHUB_COMPARE_URL = 'https://api.github.com/repos/jeffvli/feishin/compare';
|
const GITHUB_COMPARE_URL = 'https://api.github.com/repos/jeffvli/feishin/compare';
|
||||||
const RELEASES_TO_FETCH = 30;
|
|
||||||
|
|
||||||
interface GitHubCompareCommit {
|
interface GitHubCompareCommit {
|
||||||
commit: {
|
commit: {
|
||||||
@@ -37,14 +43,6 @@ interface GitHubCompareResponse {
|
|||||||
total_commits: number;
|
total_commits: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GitHubRelease {
|
|
||||||
body: null | string;
|
|
||||||
name: null | string;
|
|
||||||
prerelease: boolean;
|
|
||||||
published_at: string;
|
|
||||||
tag_name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ReleaseNotesContentProps {
|
interface ReleaseNotesContentProps {
|
||||||
onDismiss: () => void;
|
onDismiss: () => void;
|
||||||
version: string;
|
version: string;
|
||||||
@@ -54,30 +52,13 @@ function isAlphaVersion(version: string): boolean {
|
|||||||
return version.includes('-alpha');
|
return version.includes('-alpha');
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseVersionFromTag(tagName: string): string {
|
|
||||||
return tagName.startsWith('v') ? tagName.slice(1) : tagName;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toTag(version: string): string {
|
|
||||||
return version.startsWith('v') ? version : `v${version}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ReleaseNotesContent = ({ onDismiss, version }: ReleaseNotesContentProps) => {
|
const ReleaseNotesContent = ({ onDismiss, version }: ReleaseNotesContentProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [selectedVersion, setSelectedVersion] = useState(version);
|
const [selectedVersion, setSelectedVersion] = useState(version);
|
||||||
const isAlpha = isAlphaVersion(selectedVersion);
|
const isAlpha = isAlphaVersion(selectedVersion);
|
||||||
|
|
||||||
// Fetch list of recent releases for the selector
|
// Fetch list of recent releases for the selector
|
||||||
const { data: releasesList = [] } = useQuery({
|
const { data: releasesList = [] } = useGithubReleasesList();
|
||||||
queryFn: async () => {
|
|
||||||
const response = await axios.get<GitHubRelease[]>(GITHUB_RELEASES_URL, {
|
|
||||||
params: { per_page: RELEASES_TO_FETCH },
|
|
||||||
});
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
queryKey: ['github-releases-list'],
|
|
||||||
retry: 2,
|
|
||||||
});
|
|
||||||
|
|
||||||
const latestStableRelease = useMemo(() => {
|
const latestStableRelease = useMemo(() => {
|
||||||
return releasesList.find((r) => !r.prerelease);
|
return releasesList.find((r) => !r.prerelease);
|
||||||
|
|||||||
@@ -2,11 +2,15 @@ import type { ItemListStateItem } from '/@/renderer/components/item-list/helpers
|
|||||||
import type { LibraryItem } from '/@/shared/types/domain-types';
|
import type { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
import merge from 'lodash/merge';
|
import merge from 'lodash/merge';
|
||||||
|
import semverGt from 'semver/functions/gt';
|
||||||
|
import semverValid from 'semver/functions/valid';
|
||||||
import { devtools, persist } from 'zustand/middleware';
|
import { devtools, persist } from 'zustand/middleware';
|
||||||
import { immer } from 'zustand/middleware/immer';
|
import { immer } from 'zustand/middleware/immer';
|
||||||
import { shallow } from 'zustand/shallow';
|
import { shallow } from 'zustand/shallow';
|
||||||
import { createWithEqualityFn } from 'zustand/traditional';
|
import { createWithEqualityFn } from 'zustand/traditional';
|
||||||
|
|
||||||
|
import packageJson from '../../../package.json';
|
||||||
|
|
||||||
import { AlbumListSort, SongListSort, SortOrder } from '/@/shared/types/domain-types';
|
import { AlbumListSort, SongListSort, SortOrder } from '/@/shared/types/domain-types';
|
||||||
import { Platform } from '/@/shared/types/types';
|
import { Platform } from '/@/shared/types/types';
|
||||||
|
|
||||||
@@ -24,6 +28,7 @@ export interface AppSlice extends AppState {
|
|||||||
setGenreIdsMode: (mode: 'and' | 'or') => void;
|
setGenreIdsMode: (mode: 'and' | 'or') => void;
|
||||||
setGenreSelectMode: (mode: 'multi' | 'single') => void;
|
setGenreSelectMode: (mode: 'multi' | 'single') => void;
|
||||||
setGlobalExpanded: (value: GlobalExpandedState | null) => void;
|
setGlobalExpanded: (value: GlobalExpandedState | null) => void;
|
||||||
|
setLatestVersion: (version: null | string) => void;
|
||||||
setPageSidebar: (key: string, value: boolean) => void;
|
setPageSidebar: (key: string, value: boolean) => void;
|
||||||
setPrivateMode: (enabled: boolean) => void;
|
setPrivateMode: (enabled: boolean) => void;
|
||||||
setShowTimeRemaining: (enabled: boolean) => void;
|
setShowTimeRemaining: (enabled: boolean) => void;
|
||||||
@@ -52,6 +57,7 @@ export interface AppState {
|
|||||||
genreSelectMode: 'multi' | 'single';
|
genreSelectMode: 'multi' | 'single';
|
||||||
globalExpanded: GlobalExpandedState | null;
|
globalExpanded: GlobalExpandedState | null;
|
||||||
isReorderingQueue: boolean;
|
isReorderingQueue: boolean;
|
||||||
|
latestVersion: null | string;
|
||||||
pageSidebar: Record<string, boolean>;
|
pageSidebar: Record<string, boolean>;
|
||||||
platform: Platform;
|
platform: Platform;
|
||||||
privateMode: boolean;
|
privateMode: boolean;
|
||||||
@@ -157,6 +163,11 @@ export const useAppStore = createWithEqualityFn<AppSlice>()(
|
|||||||
state.globalExpanded = value;
|
state.globalExpanded = value;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
setLatestVersion: (version) => {
|
||||||
|
set((state) => {
|
||||||
|
state.latestVersion = version;
|
||||||
|
});
|
||||||
|
},
|
||||||
setPageSidebar: (key, value) => {
|
setPageSidebar: (key, value) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.pageSidebar[key] = value;
|
state.pageSidebar[key] = value;
|
||||||
@@ -219,6 +230,7 @@ export const useAppStore = createWithEqualityFn<AppSlice>()(
|
|||||||
genreSelectMode: 'multi',
|
genreSelectMode: 'multi',
|
||||||
globalExpanded: null,
|
globalExpanded: null,
|
||||||
isReorderingQueue: false,
|
isReorderingQueue: false,
|
||||||
|
latestVersion: null,
|
||||||
pageSidebar: {
|
pageSidebar: {
|
||||||
album: true,
|
album: true,
|
||||||
song: true,
|
song: true,
|
||||||
@@ -261,7 +273,7 @@ export const useAppStore = createWithEqualityFn<AppSlice>()(
|
|||||||
name: 'store_app',
|
name: 'store_app',
|
||||||
partialize: (state) => {
|
partialize: (state) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- ignore non-persisted state
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- ignore non-persisted state
|
||||||
const { globalExpanded: _, ...rest } = state;
|
const { globalExpanded: _, latestVersion: __, ...rest } = state;
|
||||||
return rest;
|
return rest;
|
||||||
},
|
},
|
||||||
version: 5,
|
version: 5,
|
||||||
@@ -307,6 +319,18 @@ export const useGlobalExpanded = () => useAppStore((state) => state.globalExpand
|
|||||||
|
|
||||||
export const useSetGlobalExpanded = () => useAppStore((state) => state.actions.setGlobalExpanded);
|
export const useSetGlobalExpanded = () => useAppStore((state) => state.actions.setGlobalExpanded);
|
||||||
|
|
||||||
|
export const useLatestVersion = () => {
|
||||||
|
const latestVersion = useAppStore((state) => state.latestVersion);
|
||||||
|
const currentVersion = packageJson.version;
|
||||||
|
const isUpdateAvailable =
|
||||||
|
!!latestVersion &&
|
||||||
|
!!semverValid(latestVersion) &&
|
||||||
|
!!semverValid(currentVersion) &&
|
||||||
|
semverGt(latestVersion, currentVersion);
|
||||||
|
|
||||||
|
return { currentVersion, isUpdateAvailable, latestVersion };
|
||||||
|
};
|
||||||
|
|
||||||
export const useGlobalExpandedState = () => {
|
export const useGlobalExpandedState = () => {
|
||||||
const globalExpanded = useGlobalExpanded();
|
const globalExpanded = useGlobalExpanded();
|
||||||
const setGlobalExpanded = useSetGlobalExpanded();
|
const setGlobalExpanded = useSetGlobalExpanded();
|
||||||
|
|||||||
@@ -1055,6 +1055,8 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
|||||||
? state.queue.shuffled.length
|
? state.queue.shuffled.length
|
||||||
: queue.items.length;
|
: queue.items.length;
|
||||||
|
|
||||||
|
const isStopped = state.player.status === PlayerStatus.STOPPED;
|
||||||
|
|
||||||
if (repeat === PlayerRepeat.ONE) {
|
if (repeat === PlayerRepeat.ONE) {
|
||||||
// Manual next while repeat-one is active should still advance in the queue.
|
// Manual next while repeat-one is active should still advance in the queue.
|
||||||
const nextIndex = Math.min(playbackLength - 1, currentIndex + 1);
|
const nextIndex = Math.min(playbackLength - 1, currentIndex + 1);
|
||||||
@@ -1063,6 +1065,10 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
|||||||
state.player.index = nextIndex;
|
state.player.index = nextIndex;
|
||||||
state.player.playerNum = 1;
|
state.player.playerNum = 1;
|
||||||
setTimestampStore(0);
|
setTimestampStore(0);
|
||||||
|
|
||||||
|
if (isStopped) {
|
||||||
|
state.player.status = PlayerStatus.PLAYING;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
eventEmitter.emit('MEDIA_NEXT', {
|
eventEmitter.emit('MEDIA_NEXT', {
|
||||||
@@ -1112,6 +1118,10 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
|||||||
state.player.index = nextIndex;
|
state.player.index = nextIndex;
|
||||||
state.player.playerNum = 1;
|
state.player.playerNum = 1;
|
||||||
setTimestampStore(0);
|
setTimestampStore(0);
|
||||||
|
|
||||||
|
if (isStopped) {
|
||||||
|
state.player.status = PlayerStatus.PLAYING;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
eventEmitter.emit('MEDIA_NEXT', {
|
eventEmitter.emit('MEDIA_NEXT', {
|
||||||
@@ -1246,10 +1256,17 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
|||||||
previousIndex = Math.max(0, currentIndex - 1);
|
previousIndex = Math.max(0, currentIndex - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Same Chromium Media Session pitfall as mediaNext: a STOPPED→new-src
|
||||||
|
// transition without PLAYING drops OS media-key routing.
|
||||||
|
const resumeFromStopped = get().player.status === PlayerStatus.STOPPED;
|
||||||
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.player.index = previousIndex;
|
state.player.index = previousIndex;
|
||||||
state.player.playerNum = 1;
|
state.player.playerNum = 1;
|
||||||
setTimestampStore(0);
|
setTimestampStore(0);
|
||||||
|
if (resumeFromStopped) {
|
||||||
|
state.player.status = PlayerStatus.PLAYING;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
eventEmitter.emit('MEDIA_PREV', {
|
eventEmitter.emit('MEDIA_PREV', {
|
||||||
|
|||||||
@@ -2052,7 +2052,7 @@ const initialState: SettingsState = {
|
|||||||
ALBUMARTISTSSORT: {
|
ALBUMARTISTSSORT: {
|
||||||
autocompleteSource: 'serverArtists',
|
autocompleteSource: 'serverArtists',
|
||||||
customValues: [],
|
customValues: [],
|
||||||
multiValue: true,
|
multiValue: false,
|
||||||
},
|
},
|
||||||
artist: {
|
artist: {
|
||||||
autocompleteSource: 'serverArtists',
|
autocompleteSource: 'serverArtists',
|
||||||
@@ -2067,12 +2067,12 @@ const initialState: SettingsState = {
|
|||||||
artistSort: {
|
artistSort: {
|
||||||
autocompleteSource: 'serverArtists',
|
autocompleteSource: 'serverArtists',
|
||||||
customValues: [],
|
customValues: [],
|
||||||
multiValue: true,
|
multiValue: false,
|
||||||
},
|
},
|
||||||
ARTISTSSORT: {
|
ARTISTSSORT: {
|
||||||
autocompleteSource: 'serverArtists',
|
autocompleteSource: 'serverArtists',
|
||||||
customValues: [],
|
customValues: [],
|
||||||
multiValue: true,
|
multiValue: false,
|
||||||
},
|
},
|
||||||
genre: {
|
genre: {
|
||||||
autocompleteSource: 'serverGenres',
|
autocompleteSource: 'serverGenres',
|
||||||
@@ -2705,10 +2705,26 @@ export const useSettingsStore = createWithEqualityFn<SettingsSlice>()(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (version < 32) {
|
||||||
|
const tagConfigs = state.tagEditor?.tagConfigs;
|
||||||
|
if (tagConfigs) {
|
||||||
|
for (const key of [
|
||||||
|
'albumArtistSort',
|
||||||
|
'ALBUMARTISTSSORT',
|
||||||
|
'artistSort',
|
||||||
|
'ARTISTSSORT',
|
||||||
|
] as const) {
|
||||||
|
if (tagConfigs[key]) {
|
||||||
|
tagConfigs[key].multiValue = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return persistedState;
|
return persistedState;
|
||||||
},
|
},
|
||||||
name: 'store_settings',
|
name: 'store_settings',
|
||||||
version: 31,
|
version: 32,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user