feat: v1.19.0 — NSIS Installer, Tray Removed, Storage Warning, Theme Polish

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-03-27 23:56:35 +01:00
parent 936e548f40
commit b6fb66c46d
29 changed files with 1766 additions and 1142 deletions
+10 -7
View File
@@ -1,7 +1,7 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import { Play, Star } from 'lucide-react';
import { SubsonicSong } from '../api/subsonic';
import { Track } from '../store/playerStore';
import { Track, usePlayerStore } from '../store/playerStore';
import { useTranslation } from 'react-i18next';
function formatDuration(seconds: number): string {
@@ -45,8 +45,6 @@ interface AlbumTrackListProps {
hasVariousArtists: boolean;
currentTrack: Track | null;
isPlaying: boolean;
hoveredSongId: string | null;
setHoveredSongId: (id: string | null) => void;
ratings: Record<string, number>;
starredSongs: Set<string>;
onPlaySong: (song: SubsonicSong) => void;
@@ -60,8 +58,6 @@ export default function AlbumTrackList({
hasVariousArtists,
currentTrack,
isPlaying,
hoveredSongId,
setHoveredSongId,
ratings,
starredSongs,
onPlaySong,
@@ -70,6 +66,12 @@ export default function AlbumTrackList({
onContextMenu,
}: AlbumTrackListProps) {
const { t } = useTranslation();
const [hoveredSongId, setHoveredSongId] = useState<string | null>(null);
const [contextMenuSongId, setContextMenuSongId] = useState<string | null>(null);
const contextMenuOpen = usePlayerStore(s => s.contextMenu.isOpen);
useEffect(() => {
if (!contextMenuOpen) setContextMenuSongId(null);
}, [contextMenuOpen]);
const totalDuration = songs.reduce((acc, s) => acc + s.duration, 0);
const discs = new Map<number, SubsonicSong[]>();
@@ -112,12 +114,13 @@ export default function AlbumTrackList({
{discs.get(discNum)!.map((song, i) => (
<div
key={song.id}
className={`track-row${hasVariousArtists ? ' track-row-va' : ''}${currentTrack?.id === song.id ? ' active' : ''}`}
className={`track-row${hasVariousArtists ? ' track-row-va' : ''}${currentTrack?.id === song.id ? ' active' : ''}${contextMenuSongId === song.id ? ' context-active' : ''}`}
onMouseEnter={() => setHoveredSongId(song.id)}
onMouseLeave={() => setHoveredSongId(null)}
onDoubleClick={() => onPlaySong(song)}
onContextMenu={e => {
e.preventDefault();
setContextMenuSongId(song.id);
onContextMenu(e.clientX, e.clientY, makeTrack(song), 'album-song');
}}
role="row"
+18 -3
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { getCachedUrl } from '../utils/imageCache';
interface CachedImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
@@ -17,7 +17,22 @@ export function useCachedUrl(fetchUrl: string, cacheKey: string): string {
return resolved || fetchUrl;
}
export default function CachedImage({ src, cacheKey, ...props }: CachedImageProps) {
export default function CachedImage({ src, cacheKey, style, onLoad, ...props }: CachedImageProps) {
const resolvedSrc = useCachedUrl(src, cacheKey);
return <img src={resolvedSrc} {...props} />;
const [loaded, setLoaded] = useState(false);
const prevSrc = useRef('');
if (resolvedSrc !== prevSrc.current) {
prevSrc.current = resolvedSrc;
setLoaded(false);
}
return (
<img
src={resolvedSrc}
style={{ ...style, opacity: loaded ? 1 : 0, transition: loaded ? 'opacity 0.15s ease' : 'none' }}
onLoad={e => { setLoaded(true); onLoad?.(e); }}
{...props}
/>
);
}
+90
View File
@@ -0,0 +1,90 @@
import { useState } from 'react';
import { X, Download } from 'lucide-react';
interface Props {
onConfirm: (since: number) => void;
onClose: () => void;
}
export default function ExportPickerModal({ onConfirm, onClose }: Props) {
const today = new Date().toISOString().slice(0, 10);
const [date, setDate] = useState(today);
const handleConfirm = () => {
const since = new Date(date + 'T00:00:00').getTime();
onConfirm(since);
};
return (
<div
style={{
position: 'fixed', inset: 0, zIndex: 99998,
background: 'rgba(0,0,0,0.6)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}
onClick={e => { if (e.target === e.currentTarget) onClose(); }}
>
<div style={{
background: 'var(--bg-card)',
border: '1px solid var(--border-subtle)',
borderRadius: '14px',
padding: '28px 32px',
width: '340px',
boxShadow: '0 8px 40px rgba(0,0,0,0.5)',
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '20px' }}>
<h2 style={{ margin: 0, fontSize: '16px', fontWeight: 700, color: 'var(--text-primary)' }}>
Alben exportieren
</h2>
<button
onClick={onClose}
style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-secondary)', padding: '4px', display: 'flex' }}
>
<X size={18} />
</button>
</div>
<p style={{ margin: '0 0 16px', fontSize: '13px', color: 'var(--text-secondary)', lineHeight: 1.5 }}>
Alle Alben exportieren, die seit diesem Datum hinzugekommen sind:
</p>
<input
type="date"
value={date}
max={today}
onChange={e => {
setDate(e.target.value);
e.target.blur();
}}
style={{
width: '100%',
padding: '9px 12px',
borderRadius: '8px',
border: '1px solid var(--border-subtle)',
background: 'var(--bg-app)',
color: 'var(--text-primary)',
fontSize: '14px',
boxSizing: 'border-box',
outline: 'none',
colorScheme: 'dark',
}}
/>
<div style={{ display: 'flex', gap: '10px', marginTop: '20px' }}>
<button className="btn btn-surface" onClick={onClose} style={{ flex: 1 }}>
Abbrechen
</button>
<button
className="btn btn-primary"
onClick={handleConfirm}
disabled={!date}
style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px' }}
>
<Download size={15} />
Exportieren
</button>
</div>
</div>
</div>
);
}
+4 -4
View File
@@ -16,8 +16,8 @@ const THEME_GROUPS: { group: string; themes: ThemeDef[] }[] = [
{ id: 'gw1', label: 'GW1', bg: '#0e0b08', card: '#1a1208', accent: '#c8960c' },
{ id: 'grand-theft-audio', label: 'Grand Theft Audio', bg: '#141414', card: '#0a0a0a', accent: '#57b05a' },
{ id: 'lambda-17', label: 'Lambda 17', bg: '#14171a', card: '#0a0b0c', accent: '#ff9d00' },
{ id: 'nightcity-2077', label: 'NightCity 2077', bg: '#050505', card: '#000000', accent: '#FCEE0A' },
{ id: 'tetrastack', label: 'TetraStack', bg: '#0a0a0a', card: '#151515', accent: '#00f0f0' },
{ id: 'nightcity-2077', label: 'NightCity 2077', bg: '#06060f', card: '#0a0a1a', accent: '#FCEE0A' },
{ id: 'tetrastack', label: 'TetraStack', bg: '#060614', card: '#0c0c20', accent: '#00f0f0' },
{ id: 'v-tactical', label: 'V-Tactical', bg: '#161c22', card: '#090c0e', accent: '#ff8a00' },
{ id: 'horde', label: 'Horde', bg: '#1a0500', card: '#2e0a02', accent: '#cc2200' },
{ id: 'alliance', label: 'Alliance', bg: '#06101e', card: '#0c1e34', accent: '#3388cc' },
@@ -33,7 +33,7 @@ const THEME_GROUPS: { group: string; themes: ThemeDef[] }[] = [
{ id: 'morpheus', label: 'Morpheus', bg: '#050905', card: '#0a120a', accent: '#00ff41' },
{ id: 'spider-tech', label: 'Spider-Tech', bg: '#0e0c18', card: '#181428', accent: '#E62429' },
{ id: 'stark-hud', label: 'Stark HUD', bg: '#0b0f15', card: '#05070a', accent: '#00f2ff' },
{ id: 't-800', label: 'T-800', bg: '#1f242d', card: '#0a0c10', accent: '#00d4ff' },
{ id: 't-800', label: 'T-800', bg: '#140e0e', card: '#1a0a0a', accent: '#ff2000' },
{ id: 'barb-and-ken', label: 'Barb & Ken', bg: '#1a000f', card: '#2e0019', accent: '#FF1B8D' },
{ id: 'toy-tale', label: 'Toy Tale', bg: '#1a1208', card: '#2a1c10', accent: '#FFD600' },
],
@@ -92,7 +92,7 @@ const THEME_GROUPS: { group: string; themes: ThemeDef[] }[] = [
{ id: 'p-dvd', label: 'P-DVD', bg: '#141414', card: '#000000', accent: '#00aaff' },
{ id: 'spotless', label: 'Spotless', bg: '#121212', card: '#181818', accent: '#1ED760' },
{ id: 'jayfin', label: 'Jayfin', bg: '#141414', card: '#1e1e1e', accent: '#AA5CC3' },
{ id: 'wnamp', label: 'WnAmp', bg: '#2b2b3a', card: '#000000', accent: '#00ff00' },
{ id: 'wnamp', label: 'WnAmp', bg: '#2b2b3a', card: '#000000', accent: '#d4cc46' },
],
},
{
+3 -3
View File
@@ -64,9 +64,9 @@ function drawWaveform(
ctx.clearRect(0, 0, w, h);
const style = getComputedStyle(document.documentElement);
const colorAccent = style.getPropertyValue('--accent').trim() || '#cba6f7';
const colorBuffered = style.getPropertyValue('--ctp-overlay0').trim() || '#6c7086';
const colorUnplayed = style.getPropertyValue('--ctp-surface1').trim() || '#313244';
const colorAccent = style.getPropertyValue('--waveform-played').trim() || style.getPropertyValue('--accent').trim() || '#cba6f7';
const colorBuffered = style.getPropertyValue('--waveform-buffered').trim() || style.getPropertyValue('--ctp-overlay0').trim() || '#6c7086';
const colorUnplayed = style.getPropertyValue('--waveform-unplayed').trim() || style.getPropertyValue('--ctp-surface1').trim() || '#313244';
if (!heights) {
ctx.globalAlpha = 0.3;