mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-29 07:17:44 +02:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e3d7e8e856 | |||
| 5ec8f1a904 | |||
| 83b20d9086 | |||
| 211f09fe19 | |||
| 03c1fb0ff2 | |||
| 834412ad31 |
+8
-1
@@ -18,8 +18,15 @@ FROM nginx:alpine-slim
|
|||||||
|
|
||||||
COPY --chown=nginx:nginx --from=builder /app/out/web /usr/share/nginx/html
|
COPY --chown=nginx:nginx --from=builder /app/out/web /usr/share/nginx/html
|
||||||
COPY ./settings.js.template /etc/nginx/templates/settings.js.template
|
COPY ./settings.js.template /etc/nginx/templates/settings.js.template
|
||||||
COPY ng.conf.template /etc/nginx/templates/default.conf.template
|
COPY ./ng.conf.template /etc/nginx/templates/default.conf.template
|
||||||
|
COPY ./docker-entrypoint.sh /docker-entrypoint.sh
|
||||||
|
|
||||||
|
RUN chmod +x /docker-entrypoint.sh
|
||||||
|
|
||||||
ENV PUBLIC_PATH="/"
|
ENV PUBLIC_PATH="/"
|
||||||
|
ENV LEGACY_AUTHENTICATION="false"
|
||||||
|
ENV ANALYTICS_DISABLED="false"
|
||||||
|
|
||||||
EXPOSE 9180
|
EXPOSE 9180
|
||||||
|
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|||||||
+3
-2
@@ -1,7 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
feishin:
|
feishin:
|
||||||
container_name: feishin
|
container_name: feishin
|
||||||
image: 'ghcr.io/jeffvli/feishin:latest'
|
image: ghcr.io/jeffvli/feishin:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- SERVER_NAME=jellyfin # pre-defined server name
|
- SERVER_NAME=jellyfin # pre-defined server name
|
||||||
@@ -9,6 +9,7 @@ services:
|
|||||||
- SERVER_TYPE=jellyfin # the allowed types are: jellyfin, navidrome, subsonic. These values are case insensitive
|
- SERVER_TYPE=jellyfin # the allowed types are: jellyfin, navidrome, subsonic. These values are case insensitive
|
||||||
- SERVER_URL= # http://address:port or https://address:port
|
- SERVER_URL= # http://address:port or https://address:port
|
||||||
- LEGACY_AUTHENTICATION=false # When SERVER_LOCK is true, sets the legacyauth flag for server authentication (true or false)
|
- LEGACY_AUTHENTICATION=false # When SERVER_LOCK is true, sets the legacyauth flag for server authentication (true or false)
|
||||||
|
- ANALYTICS_DISABLED=false # When true, disables analytics
|
||||||
ports:
|
ports:
|
||||||
- 9180:9180
|
- 9180:9180
|
||||||
# Alternatively, to restrict to only localhost, - 127.0.0.1:9180:8190
|
# Alternatively, to restrict to only localhost, - 127.0.0.1:9180:8190
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Set default values for environment variables if not already set
|
||||||
|
export LEGACY_AUTHENTICATION=${LEGACY_AUTHENTICATION:-false}
|
||||||
|
export ANALYTICS_DISABLED=${ANALYTICS_DISABLED:-false}
|
||||||
|
|
||||||
|
# Execute the original nginx command
|
||||||
|
exec "$@"
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
padding: var(--theme-spacing-md);
|
padding: var(--theme-spacing-md);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|||||||
@@ -100,27 +100,55 @@ export const AlbumDetailHeader = forwardRef<HTMLDivElement>((_props, ref) => {
|
|||||||
const originalDifferentFromRelease =
|
const originalDifferentFromRelease =
|
||||||
album?.originalDate && album?.originalDate !== album?.releaseDate;
|
album?.originalDate && album?.originalDate !== album?.releaseDate;
|
||||||
|
|
||||||
|
const originalYearDifferentFromRelease = album?.originalYear !== album?.releaseYear;
|
||||||
|
|
||||||
const playCount = album?.playCount;
|
const playCount = album?.playCount;
|
||||||
|
|
||||||
const releasePrefix = originalDifferentFromRelease
|
const releasePrefix = originalDifferentFromRelease
|
||||||
? t('page.albumDetail.released', { postProcess: 'sentenceCase' })
|
? t('page.albumDetail.released', { postProcess: 'sentenceCase' })
|
||||||
: '♫';
|
: '♫';
|
||||||
|
|
||||||
if (originalDifferentFromRelease && album.originalDate) {
|
const releaseYearPrefix = originalYearDifferentFromRelease
|
||||||
items.push({
|
? t('page.albumDetail.released', { postProcess: 'sentenceCase' })
|
||||||
id: 'originalDate',
|
: '♫';
|
||||||
value: `♫ ${formatDateAbsoluteUTC(album.originalDate)}`,
|
|
||||||
});
|
if (album.originalDate) {
|
||||||
|
if (originalDifferentFromRelease) {
|
||||||
|
items.push({
|
||||||
|
id: 'originalDate',
|
||||||
|
value: `♫ ${formatDateAbsoluteUTC(album.originalDate)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (releaseDate) {
|
||||||
|
items.push({
|
||||||
|
id: 'releaseDate',
|
||||||
|
value: `${releasePrefix} ${formatDateAbsoluteUTC(releaseDate)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (album.originalYear) {
|
||||||
|
if (originalYearDifferentFromRelease) {
|
||||||
|
items.push({
|
||||||
|
id: 'originalYear',
|
||||||
|
value: `♫ ${album.originalYear}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (releaseDate) {
|
||||||
|
items.push({
|
||||||
|
id: 'releaseDate',
|
||||||
|
value: `${releaseYearPrefix} ${formatDateAbsoluteUTC(releaseDate)}`,
|
||||||
|
});
|
||||||
|
} else if (releaseYear) {
|
||||||
|
items.push({
|
||||||
|
id: 'releaseYear',
|
||||||
|
value: `${releaseYearPrefix} ${releaseYear}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items.push(
|
items.push(
|
||||||
...[
|
...[
|
||||||
{
|
|
||||||
id: 'releaseDate',
|
|
||||||
value: releaseDate
|
|
||||||
? `${releasePrefix} ${formatDateAbsoluteUTC(releaseDate)}`
|
|
||||||
: releaseYear,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'songCount',
|
id: 'songCount',
|
||||||
value: t('entity.trackWithCount', { count: detailQuery?.data?.songCount || 0 }),
|
value: t('entity.trackWithCount', { count: detailQuery?.data?.songCount || 0 }),
|
||||||
|
|||||||
@@ -266,97 +266,102 @@ export const HotkeyManagerSettings = memo(() => {
|
|||||||
/>
|
/>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Table withColumnBorders withRowBorders>
|
<Table withColumnBorders withRowBorders>
|
||||||
{filteredBindings.map((binding) => (
|
<Table.Tbody>
|
||||||
<Table.Tr key={`hotkey-${binding}`}>
|
{filteredBindings.map((binding) => (
|
||||||
<Table.Td style={{ userSelect: 'none' }}>
|
<Table.Tr key={`hotkey-${binding}`}>
|
||||||
{BINDINGS_MAP[binding as keyof typeof BINDINGS_MAP]}
|
<Table.Td style={{ userSelect: 'none' }}>
|
||||||
</Table.Td>
|
{BINDINGS_MAP[binding as keyof typeof BINDINGS_MAP]}
|
||||||
<Table.Td>
|
</Table.Td>
|
||||||
<TextInput
|
<Table.Td>
|
||||||
id={`hotkey-${binding}`}
|
<TextInput
|
||||||
leftSection={<Icon icon="keyboard" />}
|
id={`hotkey-${binding}`}
|
||||||
onBlur={() => setSelected(null)}
|
leftSection={<Icon icon="keyboard" />}
|
||||||
onChange={() => {}}
|
onBlur={() => setSelected(null)}
|
||||||
onKeyDownCapture={(e) => {
|
onChange={() => {}}
|
||||||
if (selected !== (binding as BindingActions))
|
onKeyDownCapture={(e) => {
|
||||||
return;
|
if (selected !== (binding as BindingActions))
|
||||||
handleSetHotkey(binding as BindingActions, e);
|
return;
|
||||||
}}
|
handleSetHotkey(binding as BindingActions, e);
|
||||||
readOnly
|
}}
|
||||||
rightSection={
|
readOnly
|
||||||
<ActionIcon
|
rightSection={
|
||||||
icon="edit"
|
<ActionIcon
|
||||||
onClick={() => {
|
icon="edit"
|
||||||
setSelected(binding as BindingActions);
|
onClick={() => {
|
||||||
document
|
setSelected(binding as BindingActions);
|
||||||
.getElementById(`hotkey-${binding}`)
|
document
|
||||||
?.focus();
|
.getElementById(`hotkey-${binding}`)
|
||||||
|
?.focus();
|
||||||
|
}}
|
||||||
|
variant="transparent"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
opacity:
|
||||||
|
selected === (binding as BindingActions)
|
||||||
|
? 0.8
|
||||||
|
: 1,
|
||||||
|
outline: duplicateHotkeyMap.includes(
|
||||||
|
bindings[
|
||||||
|
binding as keyof typeof BINDINGS_MAP
|
||||||
|
].hotkey!,
|
||||||
|
)
|
||||||
|
? '1px dashed red'
|
||||||
|
: undefined,
|
||||||
|
}}
|
||||||
|
value={
|
||||||
|
bindings[binding as keyof typeof BINDINGS_MAP]
|
||||||
|
.hotkey
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Table.Td>
|
||||||
|
{isElectron() && (
|
||||||
|
<Table.Td>
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
bindings[
|
||||||
|
binding as keyof typeof BINDINGS_MAP
|
||||||
|
].isGlobal
|
||||||
|
}
|
||||||
|
disabled={
|
||||||
|
bindings[
|
||||||
|
binding as keyof typeof BINDINGS_MAP
|
||||||
|
].hotkey === ''
|
||||||
|
}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleSetGlobalHotkey(
|
||||||
|
binding as BindingActions,
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
opacity: bindings[
|
||||||
|
binding as keyof typeof BINDINGS_MAP
|
||||||
|
].allowGlobal
|
||||||
|
? 1
|
||||||
|
: 0,
|
||||||
}}
|
}}
|
||||||
|
/>
|
||||||
|
</Table.Td>
|
||||||
|
)}
|
||||||
|
{bindings[binding as keyof typeof BINDINGS_MAP].hotkey && (
|
||||||
|
<Table.Td>
|
||||||
|
<ActionIcon
|
||||||
|
icon="x"
|
||||||
|
iconProps={{
|
||||||
|
color: 'error',
|
||||||
|
}}
|
||||||
|
onClick={() =>
|
||||||
|
handleClearHotkey(binding as BindingActions)
|
||||||
|
}
|
||||||
variant="transparent"
|
variant="transparent"
|
||||||
/>
|
/>
|
||||||
}
|
</Table.Td>
|
||||||
style={{
|
)}
|
||||||
opacity:
|
</Table.Tr>
|
||||||
selected === (binding as BindingActions)
|
))}
|
||||||
? 0.8
|
</Table.Tbody>
|
||||||
: 1,
|
|
||||||
outline: duplicateHotkeyMap.includes(
|
|
||||||
bindings[binding as keyof typeof BINDINGS_MAP]
|
|
||||||
.hotkey!,
|
|
||||||
)
|
|
||||||
? '1px dashed red'
|
|
||||||
: undefined,
|
|
||||||
}}
|
|
||||||
value={
|
|
||||||
bindings[binding as keyof typeof BINDINGS_MAP]
|
|
||||||
.hotkey
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Table.Td>
|
|
||||||
{isElectron() && (
|
|
||||||
<Table.Td>
|
|
||||||
<Checkbox
|
|
||||||
checked={
|
|
||||||
bindings[binding as keyof typeof BINDINGS_MAP]
|
|
||||||
.isGlobal
|
|
||||||
}
|
|
||||||
disabled={
|
|
||||||
bindings[binding as keyof typeof BINDINGS_MAP]
|
|
||||||
.hotkey === ''
|
|
||||||
}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleSetGlobalHotkey(
|
|
||||||
binding as BindingActions,
|
|
||||||
e,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
size="md"
|
|
||||||
style={{
|
|
||||||
opacity: bindings[
|
|
||||||
binding as keyof typeof BINDINGS_MAP
|
|
||||||
].allowGlobal
|
|
||||||
? 1
|
|
||||||
: 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Table.Td>
|
|
||||||
)}
|
|
||||||
{bindings[binding as keyof typeof BINDINGS_MAP].hotkey && (
|
|
||||||
<Table.Td>
|
|
||||||
<ActionIcon
|
|
||||||
icon="x"
|
|
||||||
iconProps={{
|
|
||||||
color: 'error',
|
|
||||||
}}
|
|
||||||
onClick={() =>
|
|
||||||
handleClearHotkey(binding as BindingActions)
|
|
||||||
}
|
|
||||||
variant="transparent"
|
|
||||||
/>
|
|
||||||
</Table.Td>
|
|
||||||
)}
|
|
||||||
</Table.Tr>
|
|
||||||
))}
|
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
export const audiomotionanalyzerPresets = [
|
export const audiomotionanalyzerPresets = [
|
||||||
{
|
{
|
||||||
|
id: nanoid(),
|
||||||
name: 'Preset 1',
|
name: 'Preset 1',
|
||||||
value: {
|
value: {
|
||||||
alphaBars: false,
|
alphaBars: false,
|
||||||
@@ -52,6 +55,7 @@ export const audiomotionanalyzerPresets = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: nanoid(),
|
||||||
name: 'Preset 2',
|
name: 'Preset 2',
|
||||||
value: {
|
value: {
|
||||||
alphaBars: false,
|
alphaBars: false,
|
||||||
@@ -104,6 +108,7 @@ export const audiomotionanalyzerPresets = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: nanoid(),
|
||||||
name: 'Preset 3',
|
name: 'Preset 3',
|
||||||
value: {
|
value: {
|
||||||
alphaBars: false,
|
alphaBars: false,
|
||||||
@@ -158,6 +163,7 @@ export const audiomotionanalyzerPresets = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: nanoid(),
|
||||||
name: 'Preset 4',
|
name: 'Preset 4',
|
||||||
value: {
|
value: {
|
||||||
alphaBars: false,
|
alphaBars: false,
|
||||||
|
|||||||
+27
-13
@@ -1,4 +1,5 @@
|
|||||||
import butterchurnPresets from 'butterchurn-presets';
|
import butterchurnPresets from 'butterchurn-presets';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
@@ -390,10 +391,10 @@ const PresetSettings = () => {
|
|||||||
const [isPasting, setIsPasting] = useState(false);
|
const [isPasting, setIsPasting] = useState(false);
|
||||||
const [pasteValue, setPasteValue] = useState('');
|
const [pasteValue, setPasteValue] = useState('');
|
||||||
|
|
||||||
const applyPreset = (presetName: null | string) => {
|
const applyPreset = (presetId: null | string) => {
|
||||||
if (!presetName) return;
|
if (!presetId) return;
|
||||||
|
|
||||||
const preset = visualizer.audiomotionanalyzer.presets.find((p) => p.name === presetName);
|
const preset = visualizer.audiomotionanalyzer.presets.find((p) => p.id === presetId);
|
||||||
|
|
||||||
if (!preset) return;
|
if (!preset) return;
|
||||||
|
|
||||||
@@ -484,7 +485,7 @@ const PresetSettings = () => {
|
|||||||
if (existingPreset) {
|
if (existingPreset) {
|
||||||
// Update existing preset
|
// Update existing preset
|
||||||
const updatedPresets = visualizer.audiomotionanalyzer.presets.map((p) =>
|
const updatedPresets = visualizer.audiomotionanalyzer.presets.map((p) =>
|
||||||
p.name === newPresetName.trim()
|
p.id === existingPreset.id
|
||||||
? {
|
? {
|
||||||
...p,
|
...p,
|
||||||
value: getCurrentSettingsAsPresetValue(),
|
value: getCurrentSettingsAsPresetValue(),
|
||||||
@@ -499,9 +500,12 @@ const PresetSettings = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setSelectedPreset(existingPreset.id);
|
||||||
} else {
|
} else {
|
||||||
// Add new preset
|
// Add new preset
|
||||||
const newPreset = {
|
const newPreset = {
|
||||||
|
id: nanoid(),
|
||||||
name: newPresetName.trim(),
|
name: newPresetName.trim(),
|
||||||
value: getCurrentSettingsAsPresetValue(),
|
value: getCurrentSettingsAsPresetValue(),
|
||||||
};
|
};
|
||||||
@@ -513,11 +517,12 @@ const PresetSettings = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setSelectedPreset(newPreset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
setNewPresetName('');
|
setNewPresetName('');
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
setSelectedPreset(newPresetName.trim());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCurrentSettingsAsPresetValue = () => {
|
const getCurrentSettingsAsPresetValue = () => {
|
||||||
@@ -579,12 +584,17 @@ const PresetSettings = () => {
|
|||||||
const handleUpdatePreset = () => {
|
const handleUpdatePreset = () => {
|
||||||
if (!selectedPreset || !newPresetName.trim()) return;
|
if (!selectedPreset || !newPresetName.trim()) return;
|
||||||
|
|
||||||
|
const selectedPresetObj = visualizer.audiomotionanalyzer.presets.find(
|
||||||
|
(p) => p.id === selectedPreset,
|
||||||
|
);
|
||||||
|
if (!selectedPresetObj) return;
|
||||||
|
|
||||||
let trimmedName = newPresetName.trim();
|
let trimmedName = newPresetName.trim();
|
||||||
const isRenaming = trimmedName !== selectedPreset;
|
const isRenaming = trimmedName !== selectedPresetObj.name;
|
||||||
|
|
||||||
if (isRenaming) {
|
if (isRenaming) {
|
||||||
const existingNames = visualizer.audiomotionanalyzer.presets
|
const existingNames = visualizer.audiomotionanalyzer.presets
|
||||||
.filter((p) => p.name !== selectedPreset)
|
.filter((p) => p.id !== selectedPreset)
|
||||||
.map((p) => p.name);
|
.map((p) => p.name);
|
||||||
|
|
||||||
if (existingNames.includes(trimmedName)) {
|
if (existingNames.includes(trimmedName)) {
|
||||||
@@ -600,7 +610,7 @@ const PresetSettings = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updatedPresets = visualizer.audiomotionanalyzer.presets.map((p) =>
|
const updatedPresets = visualizer.audiomotionanalyzer.presets.map((p) =>
|
||||||
p.name === selectedPreset
|
p.id === selectedPreset
|
||||||
? {
|
? {
|
||||||
...p,
|
...p,
|
||||||
name: trimmedName,
|
name: trimmedName,
|
||||||
@@ -621,14 +631,13 @@ const PresetSettings = () => {
|
|||||||
|
|
||||||
setNewPresetName('');
|
setNewPresetName('');
|
||||||
setIsRenaming(false);
|
setIsRenaming(false);
|
||||||
setSelectedPreset(trimmedName);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeletePreset = () => {
|
const handleDeletePreset = () => {
|
||||||
if (!selectedPreset) return;
|
if (!selectedPreset) return;
|
||||||
|
|
||||||
const updatedPresets = visualizer.audiomotionanalyzer.presets.filter(
|
const updatedPresets = visualizer.audiomotionanalyzer.presets.filter(
|
||||||
(p) => p.name !== selectedPreset,
|
(p) => p.id !== selectedPreset,
|
||||||
);
|
);
|
||||||
|
|
||||||
setSettings({
|
setSettings({
|
||||||
@@ -797,7 +806,7 @@ const PresetSettings = () => {
|
|||||||
const presetOptions = useMemo(() => {
|
const presetOptions = useMemo(() => {
|
||||||
return visualizer.audiomotionanalyzer.presets.map((preset) => ({
|
return visualizer.audiomotionanalyzer.presets.map((preset) => ({
|
||||||
label: preset.name,
|
label: preset.name,
|
||||||
value: preset.name,
|
value: preset.id,
|
||||||
}));
|
}));
|
||||||
}, [visualizer.audiomotionanalyzer.presets]);
|
}, [visualizer.audiomotionanalyzer.presets]);
|
||||||
|
|
||||||
@@ -907,8 +916,13 @@ const PresetSettings = () => {
|
|||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setNewPresetName(selectedPreset);
|
const preset = visualizer.audiomotionanalyzer.presets.find(
|
||||||
setIsRenaming(true);
|
(p) => p.id === selectedPreset,
|
||||||
|
);
|
||||||
|
if (preset) {
|
||||||
|
setNewPresetName(preset.name);
|
||||||
|
setIsRenaming(true);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
variant="default"
|
variant="default"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -130,7 +130,12 @@ const ReleaseNotesContent = ({ onDismiss, version }: ReleaseNotesContentProps) =
|
|||||||
height: '400px',
|
height: '400px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div dangerouslySetInnerHTML={{ __html: sanitizedHtml }} />
|
<Text
|
||||||
|
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
|
||||||
|
fw={400}
|
||||||
|
lh="1.5"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
<Group justify="flex-end">
|
<Group justify="flex-end">
|
||||||
<Button
|
<Button
|
||||||
@@ -151,11 +156,13 @@ const ReleaseNotesContent = ({ onDismiss, version }: ReleaseNotesContentProps) =
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const WAIT_FOR_LOCAL_STORAGE = 1000 * 2;
|
||||||
|
|
||||||
export const ReleaseNotesModal = () => {
|
export const ReleaseNotesModal = () => {
|
||||||
const { version } = packageJson;
|
const { version } = packageJson;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [value, setValue] = useLocalStorage({ key: 'version' });
|
const [, setValue] = useLocalStorage({ key: 'version' });
|
||||||
|
|
||||||
const handleDismiss = useCallback(() => {
|
const handleDismiss = useCallback(() => {
|
||||||
setValue(version);
|
setValue(version);
|
||||||
@@ -163,25 +170,28 @@ export const ReleaseNotesModal = () => {
|
|||||||
}, [setValue, version]);
|
}, [setValue, version]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If value is undefined, set it to current version but don't show modal
|
const timeoutId = setTimeout(() => {
|
||||||
if (value === undefined) {
|
const valueFromLocalStorage = localStorage.getItem('version');
|
||||||
setValue(version);
|
const versionString = `"${version}"`;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only show modal if the stored version is different from current version
|
// Only show modal if the stored version is different from current version
|
||||||
if (value !== version) {
|
if (valueFromLocalStorage !== versionString) {
|
||||||
openModal({
|
openModal({
|
||||||
children: <ReleaseNotesContent onDismiss={handleDismiss} version={version} />,
|
children: <ReleaseNotesContent onDismiss={handleDismiss} version={version} />,
|
||||||
onClose: handleDismiss,
|
onClose: handleDismiss,
|
||||||
size: 'xl',
|
size: 'xl',
|
||||||
title: t('common.newVersion', {
|
title: t('common.newVersion', {
|
||||||
postProcess: 'sentenceCase',
|
postProcess: 'sentenceCase',
|
||||||
version,
|
version,
|
||||||
}) as string,
|
}) as string,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [handleDismiss, value, version, t, setValue]);
|
}, WAIT_FOR_LOCAL_STORAGE);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
};
|
||||||
|
}, [handleDismiss, t, version]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import merge from 'lodash/merge';
|
import mergeWith from 'lodash/mergeWith';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
import { generatePath } from 'react-router';
|
import { generatePath } from 'react-router';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { devtools, persist } from 'zustand/middleware';
|
import { devtools, persist } from 'zustand/middleware';
|
||||||
@@ -51,7 +52,16 @@ const deepMergeIntoState = <T extends Record<string, any>>(
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { actions, ...updatesWithoutActions } = updates as any;
|
const { actions, ...updatesWithoutActions } = updates as any;
|
||||||
|
|
||||||
merge(state, updatesWithoutActions);
|
// Use mergeWith to replace arrays instead of merging them by index
|
||||||
|
mergeWith(state, updatesWithoutActions, (_objValue, srcValue) => {
|
||||||
|
// If source value is an array, replace the entire array instead of merging
|
||||||
|
if (Array.isArray(srcValue)) {
|
||||||
|
return srcValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default merge behavior
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const HomeItemSchema = z.enum([
|
const HomeItemSchema = z.enum([
|
||||||
@@ -331,6 +341,7 @@ const AudioMotionAnalyzerSettingsSchema = z.object({
|
|||||||
peakLine: z.boolean(),
|
peakLine: z.boolean(),
|
||||||
presets: z.array(
|
presets: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
value: z.any(),
|
value: z.any(),
|
||||||
}),
|
}),
|
||||||
@@ -1928,10 +1939,29 @@ export const useSettingsStore = createWithEqualityFn<SettingsSlice>()(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (version <= 19) {
|
||||||
|
// Add IDs to presets that don't have them
|
||||||
|
if (
|
||||||
|
state.visualizer?.audiomotionanalyzer?.presets &&
|
||||||
|
Array.isArray(state.visualizer.audiomotionanalyzer.presets)
|
||||||
|
) {
|
||||||
|
state.visualizer.audiomotionanalyzer.presets =
|
||||||
|
state.visualizer.audiomotionanalyzer.presets.map((preset) => {
|
||||||
|
if (!preset.id) {
|
||||||
|
return {
|
||||||
|
...preset,
|
||||||
|
id: nanoid(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return preset;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return persistedState;
|
return persistedState;
|
||||||
},
|
},
|
||||||
name: 'store_settings',
|
name: 'store_settings',
|
||||||
version: 19,
|
version: 20,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -37,36 +37,89 @@ const matchesFullDate = (date: string) => {
|
|||||||
return Boolean(date.match(/^\d{4}-\d{2}-\d{2}$/));
|
return Boolean(date.match(/^\d{4}-\d{2}-\d{2}$/));
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeReleaseDate = (item: { date?: string; releaseDate?: string }) => {
|
const matchesYearOnly = (date: string) => {
|
||||||
|
return Boolean(date.match(/^\d{4}$/));
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeReleaseDate = (item: {
|
||||||
|
date?: string;
|
||||||
|
releaseDate?: string;
|
||||||
|
}): { date: null | string; year: null | number } => {
|
||||||
if (item.releaseDate && matchesFullDate(item.releaseDate)) {
|
if (item.releaseDate && matchesFullDate(item.releaseDate)) {
|
||||||
return item.releaseDate;
|
return {
|
||||||
|
date: item.releaseDate,
|
||||||
|
year: parseInt(item.releaseDate.split('-')[0]),
|
||||||
|
};
|
||||||
|
} else if (item.releaseDate && matchesYearOnly(item.releaseDate)) {
|
||||||
|
return {
|
||||||
|
date: null,
|
||||||
|
year: parseInt(item.releaseDate),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.date && matchesFullDate(item.date)) {
|
if (item.date && matchesFullDate(item.date)) {
|
||||||
return item.date;
|
return {
|
||||||
|
date: item.date,
|
||||||
|
year: parseInt(item.date.split('-')[0]),
|
||||||
|
};
|
||||||
|
} else if (item.date && matchesYearOnly(item.date)) {
|
||||||
|
return {
|
||||||
|
date: null,
|
||||||
|
year: parseInt(item.date),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return {
|
||||||
|
date: null,
|
||||||
|
year: null,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeOriginalDate = (item: {
|
const normalizeOriginalDate = (item: {
|
||||||
date?: string;
|
date?: string;
|
||||||
originalDate?: string;
|
originalDate?: string;
|
||||||
releaseDate?: string;
|
releaseDate?: string;
|
||||||
}) => {
|
}): { date: null | string; year: null | number } => {
|
||||||
if (item.originalDate && matchesFullDate(item.originalDate)) {
|
if (item.originalDate && matchesFullDate(item.originalDate)) {
|
||||||
return item.originalDate;
|
return {
|
||||||
|
date: item.originalDate,
|
||||||
|
year: parseInt(item.originalDate.split('-')[0]),
|
||||||
|
};
|
||||||
|
} else if (item.originalDate && matchesYearOnly(item.originalDate)) {
|
||||||
|
return {
|
||||||
|
date: null,
|
||||||
|
year: parseInt(item.originalDate),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.releaseDate && matchesFullDate(item.releaseDate)) {
|
if (item.releaseDate && matchesFullDate(item.releaseDate)) {
|
||||||
return item.releaseDate;
|
return {
|
||||||
|
date: item.releaseDate,
|
||||||
|
year: parseInt(item.releaseDate.split('-')[0]),
|
||||||
|
};
|
||||||
|
} else if (item.releaseDate && matchesYearOnly(item.releaseDate)) {
|
||||||
|
return {
|
||||||
|
date: null,
|
||||||
|
year: parseInt(item.releaseDate),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.date && matchesFullDate(item.date)) {
|
if (item.date && matchesFullDate(item.date)) {
|
||||||
return item.date;
|
return {
|
||||||
|
date: item.date,
|
||||||
|
year: parseInt(item.date.split('-')[0]),
|
||||||
|
};
|
||||||
|
} else if (item.date && matchesYearOnly(item.date)) {
|
||||||
|
return {
|
||||||
|
date: null,
|
||||||
|
year: parseInt(item.date),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return {
|
||||||
|
date: null,
|
||||||
|
year: null,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getArtists = (
|
const getArtists = (
|
||||||
@@ -240,7 +293,7 @@ const normalizeSong = (
|
|||||||
: null,
|
: null,
|
||||||
playCount: item.playCount || 0,
|
playCount: item.playCount || 0,
|
||||||
playlistItemId,
|
playlistItemId,
|
||||||
releaseDate: normalizeReleaseDate(item),
|
releaseDate: normalizeReleaseDate(item).date,
|
||||||
releaseYear: item.year || null,
|
releaseYear: item.year || null,
|
||||||
sampleRate: item.sampleRate || null,
|
sampleRate: item.sampleRate || null,
|
||||||
size: item.size,
|
size: item.size,
|
||||||
@@ -303,9 +356,7 @@ const normalizeAlbum = (
|
|||||||
pathReplaceWith?: string,
|
pathReplaceWith?: string,
|
||||||
): Album => {
|
): Album => {
|
||||||
const releaseDate = normalizeReleaseDate(item);
|
const releaseDate = normalizeReleaseDate(item);
|
||||||
const releaseYear = releaseDate ? parseInt(releaseDate.split('-')[0]) : null;
|
|
||||||
const originalDate = normalizeOriginalDate(item);
|
const originalDate = normalizeOriginalDate(item);
|
||||||
const originalYear = originalDate ? parseInt(originalDate.split('-')[0]) : null;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...parseAlbumTags(item),
|
...parseAlbumTags(item),
|
||||||
@@ -341,12 +392,12 @@ const normalizeAlbum = (
|
|||||||
lastPlayedAt: normalizePlayDate(item),
|
lastPlayedAt: normalizePlayDate(item),
|
||||||
mbzId: item.mbzAlbumId || null,
|
mbzId: item.mbzAlbumId || null,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
originalDate,
|
originalDate: originalDate.date,
|
||||||
originalYear,
|
originalYear: originalDate.year,
|
||||||
playCount: item.playCount || 0,
|
playCount: item.playCount || 0,
|
||||||
releaseDate,
|
releaseDate: releaseDate.date,
|
||||||
releaseType: item.mbzAlbumType || null,
|
releaseType: item.mbzAlbumType || null,
|
||||||
releaseYear,
|
releaseYear: releaseDate.year,
|
||||||
size: item.size,
|
size: item.size,
|
||||||
songCount: item.songCount,
|
songCount: item.songCount,
|
||||||
songs: item.songs
|
songs: item.songs
|
||||||
|
|||||||
Reference in New Issue
Block a user