chore: prepare release v1.0.1 and ignore CLAUDE.md

This commit is contained in:
Psychotoxical
2026-03-11 21:56:26 +01:00
parent 730eb877ea
commit 6456b3e561
35 changed files with 1355 additions and 740 deletions
+125 -137
View File
@@ -7,13 +7,23 @@ import { useAuthStore } from '../store/authStore';
import { open } from '@tauri-apps/plugin-shell';
import { writeFile } from '@tauri-apps/plugin-fs';
import { join } from '@tauri-apps/api/path';
import { useTranslation } from 'react-i18next';
function sanitizeFilename(name: string): string {
return name
.replace(/[/\\?%*:|"<>]/g, '-')
.replace(/\.{2,}/g, '.')
.replace(/^[\s.]+|[\s.]+$/g, '')
.substring(0, 200) || 'download';
}
export default function ContextMenu() {
const { t } = useTranslation();
const { contextMenu, closeContextMenu, playTrack, enqueue, queue, currentTrack, removeTrack } = usePlayerStore();
const auth = useAuthStore();
const navigate = useNavigate();
const menuRef = useRef<HTMLDivElement>(null);
// Adjusted coordinates to keep menu on screen
const [coords, setCoords] = useState({ x: 0, y: 0 });
@@ -28,37 +38,14 @@ export default function ContextMenu() {
const rect = menuRef.current.getBoundingClientRect();
const winW = window.innerWidth;
const winH = window.innerHeight;
let finalX = contextMenu.x;
let finalY = contextMenu.y;
if (finalX + rect.width > winW) finalX = winW - rect.width - 10;
if (finalY + rect.height > winH) finalY = winH - rect.height - 10;
setCoords({ x: finalX, y: finalY });
}
}, [contextMenu.isOpen, contextMenu.x, contextMenu.y]);
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
closeContextMenu();
}
};
const handleEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') closeContextMenu();
};
if (contextMenu.isOpen) {
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('keydown', handleEsc);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('keydown', handleEsc);
};
}, [contextMenu.isOpen, closeContextMenu]);
if (!contextMenu.isOpen || !contextMenu.item) return null;
const { type, item, queueIndex } = contextMenu;
@@ -91,139 +78,140 @@ export default function ContextMenu() {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const blob = await response.blob();
if (auth.downloadFolder) {
const buffer = await blob.arrayBuffer();
const path = await join(auth.downloadFolder, `${albumName}.zip`);
const path = await join(auth.downloadFolder, `${sanitizeFilename(albumName)}.zip`);
await writeFile(path, new Uint8Array(buffer));
} else {
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = `${albumName}.zip`;
a.download = `${sanitizeFilename(albumName)}.zip`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(blobUrl), 2000);
}
} catch (e) {
console.error('Download fehlgeschlagen:', e);
console.error('Download failed:', e);
}
};
return (
<div
ref={menuRef}
className="context-menu animate-fade-in"
style={{ left: coords.x, top: coords.y }}
>
{(type === 'song' || type === 'album-song') && (() => {
const song = item as Track;
return (
<>
<div className="context-menu-item" onClick={() => handleAction(() => playTrack(song, [song]))}>
<Play size={14} /> Direkt abspielen
</div>
<div className="context-menu-item" onClick={() => handleAction(() => {
if (!currentTrack) {
playTrack(song, [song]);
return;
}
const currentIdx = usePlayerStore.getState().queueIndex;
const newQueue = [...queue];
newQueue.splice(currentIdx + 1, 0, song);
usePlayerStore.setState({ queue: newQueue });
})}>
<ChevronRight size={14} /> Als Nächstes abspielen
</div>
<div className="context-menu-item" onClick={() => handleAction(() => enqueue([song]))}>
<ListPlus size={14} /> Zur Warteschlange hinzufügen
</div>
{type === 'album-song' && (
<div className="context-menu-item" onClick={() => handleAction(async () => {
const albumData = await getAlbum(song.albumId);
const tracks = albumData.songs.map(s => ({
id: s.id, title: s.title, artist: s.artist, album: s.album,
albumId: s.albumId, duration: s.duration, coverArt: s.coverArt, track: s.track,
year: s.year, bitRate: s.bitRate, suffix: s.suffix, userRating: s.userRating,
}));
enqueue(tracks);
})}>
<ListPlus size={14} /> Ganzes Album einreihen
<>
{/* Transparent backdrop — catches all outside clicks cleanly, preventing freeze */}
<div
style={{ position: 'fixed', inset: 0, zIndex: 998 }}
onMouseDown={() => closeContextMenu()}
/>
<div
ref={menuRef}
className="context-menu animate-fade-in"
style={{ left: coords.x, top: coords.y, zIndex: 999 }}
>
{(type === 'song' || type === 'album-song') && (() => {
const song = item as Track;
return (
<>
<div className="context-menu-item" onClick={() => handleAction(() => playTrack(song, [song]))}>
<Play size={14} /> {t('contextMenu.playNow')}
</div>
)}
<div className="context-menu-divider" />
<div className="context-menu-item" onClick={() => handleAction(() => startRadio(song.artist, song.artist))}>
<Radio size={14} /> Song-Radio starten
</div>
<div className="context-menu-item" onClick={() => handleAction(() => star(song.id, 'song'))}>
<Star size={14} /> Favorisieren
</div>
</>
);
})()}
<div className="context-menu-item" onClick={() => handleAction(() => {
if (!currentTrack) {
playTrack(song, [song]);
return;
}
const currentIdx = usePlayerStore.getState().queueIndex;
const newQueue = [...queue];
newQueue.splice(currentIdx + 1, 0, song);
usePlayerStore.setState({ queue: newQueue });
})}>
<ChevronRight size={14} /> {t('contextMenu.playNext')}
</div>
<div className="context-menu-item" onClick={() => handleAction(() => enqueue([song]))}>
<ListPlus size={14} /> {t('contextMenu.addToQueue')}
</div>
{type === 'album-song' && (
<div className="context-menu-item" onClick={() => handleAction(async () => {
const albumData = await getAlbum(song.albumId);
const tracks = albumData.songs.map(s => ({
id: s.id, title: s.title, artist: s.artist, album: s.album,
albumId: s.albumId, duration: s.duration, coverArt: s.coverArt, track: s.track,
year: s.year, bitRate: s.bitRate, suffix: s.suffix, userRating: s.userRating,
}));
enqueue(tracks);
})}>
<ListPlus size={14} /> {t('contextMenu.enqueueAlbum')}
</div>
)}
<div className="context-menu-divider" />
<div className="context-menu-item" onClick={() => handleAction(() => startRadio(song.artist, song.artist))}>
<Radio size={14} /> {t('contextMenu.startRadio')}
</div>
<div className="context-menu-item" onClick={() => handleAction(() => star(song.id, 'song'))}>
<Star size={14} /> {t('contextMenu.favorite')}
</div>
</>
);
})()}
{type === 'album' && (() => {
const album = item as SubsonicAlbum;
return (
<>
<div className="context-menu-item" onClick={() => handleAction(() => {
// we don't have tracks here immediately, so we'd navigate or fetch. For now, navigate.
navigate(`/album/${album.id}`);
})}>
<Play size={14} /> Album öffnen
</div>
<div className="context-menu-divider" />
<div className="context-menu-item" onClick={() => handleAction(() => navigate(`/artist/${album.artistId}`))}>
<User size={14} /> Zum Künstler
</div>
<div className="context-menu-item" onClick={() => handleAction(() => star(album.id, 'album'))}>
<Star size={14} /> Album favorisieren
</div>
<div className="context-menu-item" onClick={() => handleAction(() => downloadAlbum(album.name, album.id))}>
<Download size={14} /> Herunterladen (ZIP)
</div>
</>
);
})()}
{type === 'album' && (() => {
const album = item as SubsonicAlbum;
return (
<>
<div className="context-menu-item" onClick={() => handleAction(() => navigate(`/album/${album.id}`))}>
<Play size={14} /> {t('contextMenu.openAlbum')}
</div>
<div className="context-menu-divider" />
<div className="context-menu-item" onClick={() => handleAction(() => navigate(`/artist/${album.artistId}`))}>
<User size={14} /> {t('contextMenu.goToArtist')}
</div>
<div className="context-menu-item" onClick={() => handleAction(() => star(album.id, 'album'))}>
<Star size={14} /> {t('contextMenu.favoriteAlbum')}
</div>
<div className="context-menu-item" onClick={() => handleAction(() => downloadAlbum(album.name, album.id))}>
<Download size={14} /> {t('contextMenu.download')}
</div>
</>
);
})()}
{type === 'artist' && (() => {
const artist = item as SubsonicArtist;
return (
<>
<div className="context-menu-item" onClick={() => handleAction(() => startRadio(artist.id, artist.name))}>
<Radio size={14} /> Künstler-Radio starten
</div>
<div className="context-menu-divider" />
<div className="context-menu-item" onClick={() => handleAction(() => star(artist.id, 'artist'))}>
<Star size={14} /> Künstler favorisieren
</div>
</>
);
})()}
{type === 'artist' && (() => {
const artist = item as SubsonicArtist;
return (
<>
<div className="context-menu-item" onClick={() => handleAction(() => startRadio(artist.id, artist.name))}>
<Radio size={14} /> {t('contextMenu.startRadio')}
</div>
<div className="context-menu-divider" />
<div className="context-menu-item" onClick={() => handleAction(() => star(artist.id, 'artist'))}>
<Star size={14} /> {t('contextMenu.favoriteArtist')}
</div>
</>
);
})()}
{type === 'queue-item' && (() => {
const song = item as Track;
return (
<>
<div className="context-menu-item" onClick={() => handleAction(() => playTrack(song, queue))}>
<Play size={14} /> Direkt abspielen
</div>
<div className="context-menu-item" style={{ color: 'var(--danger)' }} onClick={() => handleAction(() => {
if (queueIndex !== undefined) removeTrack(queueIndex);
})}>
Diesen Song entfernen
</div>
<div className="context-menu-divider" />
<div className="context-menu-item" onClick={() => handleAction(() => startRadio(song.artist, song.artist))}>
<Radio size={14} /> Song-Radio starten
</div>
</>
);
})()}
</div>
{type === 'queue-item' && (() => {
const song = item as Track;
return (
<>
<div className="context-menu-item" onClick={() => handleAction(() => playTrack(song, queue))}>
<Play size={14} /> {t('contextMenu.playNow')}
</div>
<div className="context-menu-item" style={{ color: 'var(--danger)' }} onClick={() => handleAction(() => {
if (queueIndex !== undefined) removeTrack(queueIndex);
})}>
{t('contextMenu.removeFromQueue')}
</div>
<div className="context-menu-divider" />
<div className="context-menu-item" onClick={() => handleAction(() => startRadio(song.artist, song.artist))}>
<Radio size={14} /> {t('contextMenu.startRadio')}
</div>
</>
);
})()}
</div>
</>
);
}