add stats tracking + stats page + fix scrolling bug in dynamic playlists tray

This commit is contained in:
Boof2015
2026-07-28 19:25:30 -04:00
parent b28a018ca0
commit 00d0fc4b2f
40 changed files with 5280 additions and 126 deletions
+5
View File
@@ -123,6 +123,7 @@ interface LibraryStore {
loadPreviousArtists: () => Promise<void>;
jumpToSection: (cursor: string) => Promise<boolean>;
recordTrackPlayed: (path: string) => Promise<void>;
refreshRecentlyPlayed: () => Promise<void>;
recomputeArtists: () => void;
recomputeAlbums: () => void;
setViewMode: (mode: ViewMode) => void;
@@ -907,6 +908,10 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
set({ recentlyPlayedTracks: await AstraLibraryData.getRecentlyPlayed<DbTrack>(20) });
},
refreshRecentlyPlayed: async () => {
set({ recentlyPlayedTracks: await AstraLibraryData.getRecentlyPlayed<DbTrack>(20) });
},
recomputeArtists: () => {
void resetArtists();
},
+97
View File
@@ -0,0 +1,97 @@
import { create } from 'zustand';
import { AstraLibraryData } from '../../modules/astra-library-scanner';
import { useSettingsStore } from './settingsStore';
import type {
ListeningStatsCategory,
ListeningStatsDashboard,
ListeningStatsRange,
ListeningStatsRankingMetric,
} from '@/types/listeningStats';
interface ListeningStatsStore {
range: ListeningStatsRange;
rankingMetric: ListeningStatsRankingMetric;
category: ListeningStatsCategory;
dashboard: ListeningStatsDashboard | null;
homePreview: ListeningStatsDashboard | null;
loading: boolean;
refreshing: boolean;
error: string | null;
setRange: (range: ListeningStatsRange) => void;
setRankingMetric: (metric: ListeningStatsRankingMetric) => void;
setCategory: (category: ListeningStatsCategory) => void;
loadDashboard: () => Promise<void>;
loadHomePreview: () => Promise<void>;
}
let dashboardRequest = 0;
let homeRequest = 0;
function errorMessage(error: unknown): string {
return error instanceof Error && error.message ? error.message : 'Listening Stats could not load.';
}
async function queryDashboard(
range: ListeningStatsRange,
rankingMetric: ListeningStatsRankingMetric,
): Promise<ListeningStatsDashboard> {
await AstraLibraryData.initialize();
return AstraLibraryData.getListeningStatsDashboard<ListeningStatsDashboard>({
range,
rankingMetric,
artistGroupingMode: useSettingsStore.getState().artistGroupingMode,
});
}
export const useListeningStatsStore = create<ListeningStatsStore>((set, get) => ({
range: '30d',
rankingMetric: 'plays',
category: 'tracks',
dashboard: null,
homePreview: null,
loading: false,
refreshing: false,
error: null,
setRange: (range) => {
if (get().range === range) return;
set({ range });
void get().loadDashboard();
},
setRankingMetric: (rankingMetric) => {
if (get().rankingMetric === rankingMetric) return;
set({ rankingMetric });
void get().loadDashboard();
},
setCategory: (category) => set({ category }),
loadDashboard: async () => {
const request = ++dashboardRequest;
set((state) => ({
loading: state.dashboard == null,
refreshing: state.dashboard != null,
error: null,
}));
try {
const { range, rankingMetric } = get();
const dashboard = await queryDashboard(range, rankingMetric);
if (request !== dashboardRequest) return;
set({ dashboard, loading: false, refreshing: false, error: null });
} catch (error) {
if (request !== dashboardRequest) return;
set({ loading: false, refreshing: false, error: errorMessage(error) });
}
},
loadHomePreview: async () => {
const request = ++homeRequest;
try {
const homePreview = await queryDashboard('7d', 'plays');
if (request === homeRequest) set({ homePreview });
} catch {
// Home remains quiet on transient stats failures; the full screen has retry UI.
}
},
}));
+28
View File
@@ -9,6 +9,11 @@ import {
parseHomeGreetingTextMode,
type HomeGreetingTextMode,
} from '@/home/homeGreeting';
import {
pauseListeningHistoryTracking,
resumeListeningHistoryTracking,
} from '@/audio/listeningHistoryTracker';
import { notifyListeningHistoryChanged } from '@/listeningStats/events';
/**
* Persisted app preferences. SQLite (settings table) is the source of truth — this
@@ -23,6 +28,7 @@ const SCOPE_STYLE_KEY = 'now_playing_scope_style';
const LYRICS_VISIBLE_KEY = 'lyrics_visible';
const NOW_PLAYING_COMPANION_KEY = 'now_playing_companion';
const HOME_GREETING_TEXT_MODE_KEY = 'home_greeting_text_mode';
const LISTENING_HISTORY_ENABLED_KEY = 'listening_history_enabled';
/** Which visualizer the now-playing scope stage shows. */
export type ScopeMode = 'spectrum' | 'scope';
@@ -61,6 +67,7 @@ interface SettingsStore {
lyricsVisible: boolean;
nowPlayingCompanion: NowPlayingCompanion;
homeGreetingTextMode: HomeGreetingTextMode;
listeningHistoryEnabled: boolean;
loaded: boolean;
load: () => Promise<void>;
setArtistGroupingMode: (mode: ArtistGroupingMode) => Promise<void>;
@@ -71,6 +78,7 @@ interface SettingsStore {
setLyricsVisible: (visible: boolean) => Promise<void>;
setNowPlayingCompanion: (companion: NowPlayingCompanion) => Promise<void>;
setHomeGreetingTextMode: (mode: HomeGreetingTextMode) => Promise<void>;
setListeningHistoryEnabled: (enabled: boolean) => Promise<void>;
}
export const useSettingsStore = create<SettingsStore>((set, get) => ({
@@ -82,6 +90,7 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
lyricsVisible: false,
nowPlayingCompanion: 'queue',
homeGreetingTextMode: 'messages',
listeningHistoryEnabled: true,
loaded: false,
load: async () => {
@@ -96,6 +105,7 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
LYRICS_VISIBLE_KEY,
NOW_PLAYING_COMPANION_KEY,
HOME_GREETING_TEXT_MODE_KEY,
LISTENING_HISTORY_ENABLED_KEY,
]);
const grouping = values[ARTIST_GROUPING_KEY] ?? null;
const includeSingles = values[INCLUDE_SINGLES_KEY] ?? null;
@@ -105,6 +115,7 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
const lyricsVisible = values[LYRICS_VISIBLE_KEY] ?? null;
const nowPlayingCompanion = values[NOW_PLAYING_COMPANION_KEY] ?? null;
const homeGreetingTextMode = values[HOME_GREETING_TEXT_MODE_KEY] ?? null;
const listeningHistoryEnabled = values[LISTENING_HISTORY_ENABLED_KEY] !== '0';
set({
artistGroupingMode: parseGroupingMode(grouping),
includeSingles: parseBoolean(includeSingles),
@@ -114,6 +125,7 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
lyricsVisible: parseBoolean(lyricsVisible),
nowPlayingCompanion: parseNowPlayingCompanion(nowPlayingCompanion),
homeGreetingTextMode: parseHomeGreetingTextMode(homeGreetingTextMode),
listeningHistoryEnabled,
loaded: true,
});
},
@@ -166,4 +178,20 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
set({ homeGreetingTextMode: nextMode });
await AstraLibraryData.setSettings({ [HOME_GREETING_TEXT_MODE_KEY]: nextMode });
},
setListeningHistoryEnabled: async (enabled) => {
if (get().listeningHistoryEnabled === enabled) return;
if (!enabled) await pauseListeningHistoryTracking();
try {
await AstraLibraryData.setSettings({
[LISTENING_HISTORY_ENABLED_KEY]: enabled ? '1' : '0',
});
set({ listeningHistoryEnabled: enabled });
notifyListeningHistoryChanged();
if (enabled) resumeListeningHistoryTracking();
} catch (error) {
if (!enabled) resumeListeningHistoryTracking();
throw error;
}
},
}));