mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
feat(albums): compilation filter toggle in All Albums
Tri-state button (all / only compilations / hide compilations) in the Albums page header, using the OpenSubsonic isCompilation tag from Navidrome. Client-side filter via useMemo, no extra server calls. Closes #65. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -71,6 +71,8 @@ export interface SubsonicAlbum {
|
|||||||
created?: string;
|
created?: string;
|
||||||
/** Present on some servers (e.g. OpenSubsonic) for album-level rating. */
|
/** Present on some servers (e.g. OpenSubsonic) for album-level rating. */
|
||||||
userRating?: number;
|
userRating?: number;
|
||||||
|
/** OpenSubsonic: true when the album is tagged as a compilation. */
|
||||||
|
isCompilation?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** OpenSubsonic `artists` / `albumArtists` entries on a child song (may include `userRating`). */
|
/** OpenSubsonic `artists` / `albumArtists` entries on a child song (may include `userRating`). */
|
||||||
|
|||||||
@@ -290,6 +290,12 @@ export const deTranslation = {
|
|||||||
yearTo: 'Bis',
|
yearTo: 'Bis',
|
||||||
yearFilterClear: 'Jahresfilter zurücksetzen',
|
yearFilterClear: 'Jahresfilter zurücksetzen',
|
||||||
yearFilterLabel: 'Jahr',
|
yearFilterLabel: 'Jahr',
|
||||||
|
compilationLabel: 'Sampler',
|
||||||
|
compilationOnly: 'Nur Sampler',
|
||||||
|
compilationHide: 'Ohne Sampler',
|
||||||
|
compilationTooltipAll: 'Alle Alben · klicken: nur Sampler',
|
||||||
|
compilationTooltipOnly: 'Nur Sampler · klicken: Sampler ausblenden',
|
||||||
|
compilationTooltipHide: 'Sampler ausgeblendet · klicken: alle zeigen',
|
||||||
select: 'Mehrfachauswahl',
|
select: 'Mehrfachauswahl',
|
||||||
startSelect: 'Mehrfachauswahl aktivieren',
|
startSelect: 'Mehrfachauswahl aktivieren',
|
||||||
cancelSelect: 'Abbrechen',
|
cancelSelect: 'Abbrechen',
|
||||||
|
|||||||
@@ -291,6 +291,12 @@ export const enTranslation = {
|
|||||||
yearTo: 'To',
|
yearTo: 'To',
|
||||||
yearFilterClear: 'Clear year filter',
|
yearFilterClear: 'Clear year filter',
|
||||||
yearFilterLabel: 'Year',
|
yearFilterLabel: 'Year',
|
||||||
|
compilationLabel: 'Compilations',
|
||||||
|
compilationOnly: 'Only compilations',
|
||||||
|
compilationHide: 'Hide compilations',
|
||||||
|
compilationTooltipAll: 'All albums · click: only compilations',
|
||||||
|
compilationTooltipOnly: 'Only compilations · click: hide compilations',
|
||||||
|
compilationTooltipHide: 'Compilations hidden · click: show all',
|
||||||
select: 'Multi-select',
|
select: 'Multi-select',
|
||||||
startSelect: 'Enable multi-select',
|
startSelect: 'Enable multi-select',
|
||||||
cancelSelect: 'Cancel',
|
cancelSelect: 'Cancel',
|
||||||
|
|||||||
@@ -291,6 +291,12 @@ export const esTranslation = {
|
|||||||
yearTo: 'Hasta',
|
yearTo: 'Hasta',
|
||||||
yearFilterClear: 'Limpiar filtro de año',
|
yearFilterClear: 'Limpiar filtro de año',
|
||||||
yearFilterLabel: 'Año',
|
yearFilterLabel: 'Año',
|
||||||
|
compilationLabel: 'Recopilatorios',
|
||||||
|
compilationOnly: 'Solo recopilatorios',
|
||||||
|
compilationHide: 'Ocultar recopilatorios',
|
||||||
|
compilationTooltipAll: 'Todos los álbumes · clic: solo recopilatorios',
|
||||||
|
compilationTooltipOnly: 'Solo recopilatorios · clic: ocultar recopilatorios',
|
||||||
|
compilationTooltipHide: 'Recopilatorios ocultos · clic: mostrar todo',
|
||||||
select: 'Selección múltiple',
|
select: 'Selección múltiple',
|
||||||
startSelect: 'Activar selección múltiple',
|
startSelect: 'Activar selección múltiple',
|
||||||
cancelSelect: 'Cancelar',
|
cancelSelect: 'Cancelar',
|
||||||
|
|||||||
@@ -290,6 +290,12 @@ export const frTranslation = {
|
|||||||
yearTo: 'À',
|
yearTo: 'À',
|
||||||
yearFilterClear: 'Effacer le filtre année',
|
yearFilterClear: 'Effacer le filtre année',
|
||||||
yearFilterLabel: 'Année',
|
yearFilterLabel: 'Année',
|
||||||
|
compilationLabel: 'Compilations',
|
||||||
|
compilationOnly: 'Uniquement compilations',
|
||||||
|
compilationHide: 'Masquer compilations',
|
||||||
|
compilationTooltipAll: 'Tous les albums · clic : uniquement compilations',
|
||||||
|
compilationTooltipOnly: 'Uniquement compilations · clic : masquer compilations',
|
||||||
|
compilationTooltipHide: 'Compilations masquées · clic : tout afficher',
|
||||||
select: 'Sélection multiple',
|
select: 'Sélection multiple',
|
||||||
startSelect: 'Activer la sélection multiple',
|
startSelect: 'Activer la sélection multiple',
|
||||||
cancelSelect: 'Annuler',
|
cancelSelect: 'Annuler',
|
||||||
|
|||||||
@@ -290,6 +290,12 @@ export const nbTranslation = {
|
|||||||
yearTo: 'Til',
|
yearTo: 'Til',
|
||||||
yearFilterClear: 'Tøm år filteret',
|
yearFilterClear: 'Tøm år filteret',
|
||||||
yearFilterLabel: 'År',
|
yearFilterLabel: 'År',
|
||||||
|
compilationLabel: 'Samleplater',
|
||||||
|
compilationOnly: 'Kun samleplater',
|
||||||
|
compilationHide: 'Skjul samleplater',
|
||||||
|
compilationTooltipAll: 'Alle album · klikk: kun samleplater',
|
||||||
|
compilationTooltipOnly: 'Kun samleplater · klikk: skjul samleplater',
|
||||||
|
compilationTooltipHide: 'Samleplater skjult · klikk: vis alle',
|
||||||
select: 'Multivalg',
|
select: 'Multivalg',
|
||||||
startSelect: 'Aktiver multivalg',
|
startSelect: 'Aktiver multivalg',
|
||||||
cancelSelect: 'Avbryt',
|
cancelSelect: 'Avbryt',
|
||||||
|
|||||||
@@ -289,6 +289,12 @@ export const nlTranslation = {
|
|||||||
yearTo: 'Tot',
|
yearTo: 'Tot',
|
||||||
yearFilterClear: 'Jaarfilter wissen',
|
yearFilterClear: 'Jaarfilter wissen',
|
||||||
yearFilterLabel: 'Jaar',
|
yearFilterLabel: 'Jaar',
|
||||||
|
compilationLabel: 'Compilaties',
|
||||||
|
compilationOnly: 'Alleen compilaties',
|
||||||
|
compilationHide: 'Compilaties verbergen',
|
||||||
|
compilationTooltipAll: 'Alle albums · klik: alleen compilaties',
|
||||||
|
compilationTooltipOnly: 'Alleen compilaties · klik: compilaties verbergen',
|
||||||
|
compilationTooltipHide: 'Compilaties verborgen · klik: toon alles',
|
||||||
select: 'Meervoudige selectie',
|
select: 'Meervoudige selectie',
|
||||||
startSelect: 'Meervoudige selectie inschakelen',
|
startSelect: 'Meervoudige selectie inschakelen',
|
||||||
cancelSelect: 'Annuleren',
|
cancelSelect: 'Annuleren',
|
||||||
|
|||||||
@@ -301,6 +301,12 @@ export const ruTranslation = {
|
|||||||
yearTo: 'По',
|
yearTo: 'По',
|
||||||
yearFilterClear: 'Сбросить год',
|
yearFilterClear: 'Сбросить год',
|
||||||
yearFilterLabel: 'Год',
|
yearFilterLabel: 'Год',
|
||||||
|
compilationLabel: 'Сборники',
|
||||||
|
compilationOnly: 'Только сборники',
|
||||||
|
compilationHide: 'Скрыть сборники',
|
||||||
|
compilationTooltipAll: 'Все альбомы · клик: только сборники',
|
||||||
|
compilationTooltipOnly: 'Только сборники · клик: скрыть сборники',
|
||||||
|
compilationTooltipHide: 'Сборники скрыты · клик: показать всё',
|
||||||
select: 'Множественный выбор',
|
select: 'Множественный выбор',
|
||||||
startSelect: 'Включить множественный выбор',
|
startSelect: 'Включить множественный выбор',
|
||||||
cancelSelect: 'Отмена',
|
cancelSelect: 'Отмена',
|
||||||
|
|||||||
@@ -289,6 +289,12 @@ export const zhTranslation = {
|
|||||||
yearTo: '到',
|
yearTo: '到',
|
||||||
yearFilterClear: '清除年份筛选',
|
yearFilterClear: '清除年份筛选',
|
||||||
yearFilterLabel: '年份',
|
yearFilterLabel: '年份',
|
||||||
|
compilationLabel: '合辑',
|
||||||
|
compilationOnly: '仅合辑',
|
||||||
|
compilationHide: '隐藏合辑',
|
||||||
|
compilationTooltipAll: '所有专辑 · 点击:仅合辑',
|
||||||
|
compilationTooltipOnly: '仅合辑 · 点击:隐藏合辑',
|
||||||
|
compilationTooltipHide: '已隐藏合辑 · 点击:显示全部',
|
||||||
select: '多选',
|
select: '多选',
|
||||||
startSelect: '启用多选',
|
startSelect: '启用多选',
|
||||||
cancelSelect: '取消',
|
cancelSelect: '取消',
|
||||||
|
|||||||
+36
-4
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
import React, { useEffect, useState, useCallback, useRef, useMemo } from 'react';
|
||||||
import AlbumCard from '../components/AlbumCard';
|
import AlbumCard from '../components/AlbumCard';
|
||||||
import GenreFilterBar from '../components/GenreFilterBar';
|
import GenreFilterBar from '../components/GenreFilterBar';
|
||||||
import { getAlbumList, getAlbumsByGenre, getAlbum, SubsonicAlbum, buildDownloadUrl } from '../api/subsonic';
|
import { getAlbumList, getAlbumsByGenre, getAlbum, SubsonicAlbum, buildDownloadUrl } from '../api/subsonic';
|
||||||
@@ -11,9 +11,10 @@ import { invoke } from '@tauri-apps/api/core';
|
|||||||
import { join } from '@tauri-apps/api/path';
|
import { join } from '@tauri-apps/api/path';
|
||||||
import { showToast } from '../utils/toast';
|
import { showToast } from '../utils/toast';
|
||||||
import { useZipDownloadStore } from '../store/zipDownloadStore';
|
import { useZipDownloadStore } from '../store/zipDownloadStore';
|
||||||
import { X, CheckSquare2, Download, HardDriveDownload, ListMusic } from 'lucide-react';
|
import { X, CheckSquare2, Download, HardDriveDownload, ListMusic, Disc3 } from 'lucide-react';
|
||||||
|
|
||||||
type SortType = 'alphabeticalByName' | 'alphabeticalByArtist';
|
type SortType = 'alphabeticalByName' | 'alphabeticalByArtist';
|
||||||
|
type CompFilter = 'all' | 'only' | 'hide';
|
||||||
|
|
||||||
const PAGE_SIZE = 30;
|
const PAGE_SIZE = 30;
|
||||||
const CURRENT_YEAR = new Date().getFullYear();
|
const CURRENT_YEAR = new Date().getFullYear();
|
||||||
@@ -44,6 +45,7 @@ export default function Albums() {
|
|||||||
const [selectedGenres, setSelectedGenres] = useState<string[]>([]);
|
const [selectedGenres, setSelectedGenres] = useState<string[]>([]);
|
||||||
const [yearFrom, setYearFrom] = useState('');
|
const [yearFrom, setYearFrom] = useState('');
|
||||||
const [yearTo, setYearTo] = useState('');
|
const [yearTo, setYearTo] = useState('');
|
||||||
|
const [compFilter, setCompFilter] = useState<CompFilter>('all');
|
||||||
const observerTarget = useRef<HTMLDivElement>(null);
|
const observerTarget = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// ── Multi-selection ──────────────────────────────────────────────────────
|
// ── Multi-selection ──────────────────────────────────────────────────────
|
||||||
@@ -68,9 +70,19 @@ export default function Albums() {
|
|||||||
setSelectedIds(new Set());
|
setSelectedIds(new Set());
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedAlbums = albums.filter(a => selectedIds.has(a.id));
|
const visibleAlbums = useMemo(() => {
|
||||||
|
if (compFilter === 'all') return albums;
|
||||||
|
if (compFilter === 'only') return albums.filter(a => a.isCompilation);
|
||||||
|
return albums.filter(a => !a.isCompilation);
|
||||||
|
}, [albums, compFilter]);
|
||||||
|
|
||||||
|
const selectedAlbums = visibleAlbums.filter(a => selectedIds.has(a.id));
|
||||||
const openContextMenu = usePlayerStore(state => state.openContextMenu);
|
const openContextMenu = usePlayerStore(state => state.openContextMenu);
|
||||||
|
|
||||||
|
const cycleCompFilter = () => {
|
||||||
|
setCompFilter(v => v === 'all' ? 'only' : v === 'only' ? 'hide' : 'all');
|
||||||
|
};
|
||||||
|
|
||||||
const handleDownloadZips = async () => {
|
const handleDownloadZips = async () => {
|
||||||
if (selectedAlbums.length === 0) return;
|
if (selectedAlbums.length === 0) return;
|
||||||
const folder = auth.downloadFolder || await requestDownloadFolder();
|
const folder = auth.downloadFolder || await requestDownloadFolder();
|
||||||
@@ -256,6 +268,26 @@ export default function Albums() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GenreFilterBar selected={selectedGenres} onSelectionChange={setSelectedGenres} />
|
<GenreFilterBar selected={selectedGenres} onSelectionChange={setSelectedGenres} />
|
||||||
|
|
||||||
|
<button
|
||||||
|
className={`btn btn-surface${compFilter !== 'all' ? ' btn-sort-active' : ''}`}
|
||||||
|
onClick={cycleCompFilter}
|
||||||
|
data-tooltip={
|
||||||
|
compFilter === 'all' ? t('albums.compilationTooltipAll')
|
||||||
|
: compFilter === 'only' ? t('albums.compilationTooltipOnly')
|
||||||
|
: t('albums.compilationTooltipHide')
|
||||||
|
}
|
||||||
|
data-tooltip-pos="bottom"
|
||||||
|
style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: '0.4rem',
|
||||||
|
...(compFilter !== 'all' ? { background: 'var(--accent)', color: 'var(--ctp-crust)' } : {}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Disc3 size={14} />
|
||||||
|
{compFilter === 'all' ? t('albums.compilationLabel')
|
||||||
|
: compFilter === 'only' ? t('albums.compilationOnly')
|
||||||
|
: t('albums.compilationHide')}
|
||||||
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -279,7 +311,7 @@ export default function Albums() {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="album-grid-wrap">
|
<div className="album-grid-wrap">
|
||||||
{albums.map(a => (
|
{visibleAlbums.map(a => (
|
||||||
<AlbumCard
|
<AlbumCard
|
||||||
key={a.id}
|
key={a.id}
|
||||||
album={a}
|
album={a}
|
||||||
|
|||||||
Reference in New Issue
Block a user