mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
refactor(offline): co-locate offline feature into features/offline
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import type { Location, NavigateFunction } from 'react-router-dom';
|
||||
import { resolveOfflineDisconnectNavAction } from '@/features/offline/utils/offlineBrowseRouting';
|
||||
|
||||
type ConnStatus = 'connected' | 'disconnected' | 'connecting' | 'unknown';
|
||||
|
||||
type OfflineAutoNavContext = {
|
||||
favoritesOfflineBrowse: boolean;
|
||||
localLibraryBrowse: boolean;
|
||||
playerStatsBrowse: boolean;
|
||||
playlistsOfflineBrowse: boolean;
|
||||
hasManualOfflineContent: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* On disconnect:
|
||||
* - No offline browse content → stay on the current page (banner only).
|
||||
* - Offline-capable route → stay and bump location state so data hooks reload.
|
||||
* - Otherwise → redirect to All Albums.
|
||||
*
|
||||
* Only runs on connection transitions, not every render.
|
||||
*/
|
||||
export function useOfflineAutoNav(
|
||||
connStatus: ConnStatus | string,
|
||||
ctx: OfflineAutoNavContext,
|
||||
location: Pick<Location, 'pathname' | 'search' | 'state'>,
|
||||
navigate: NavigateFunction,
|
||||
): void {
|
||||
const prevConnStatus = useRef(connStatus);
|
||||
useEffect(() => {
|
||||
const prev = prevConnStatus.current;
|
||||
prevConnStatus.current = connStatus;
|
||||
|
||||
if (connStatus !== 'disconnected' || prev === 'disconnected') return;
|
||||
|
||||
const action = resolveOfflineDisconnectNavAction(
|
||||
location.pathname,
|
||||
ctx.favoritesOfflineBrowse,
|
||||
ctx.localLibraryBrowse,
|
||||
ctx.playerStatsBrowse,
|
||||
ctx.playlistsOfflineBrowse,
|
||||
ctx.hasManualOfflineContent,
|
||||
);
|
||||
|
||||
if (action.kind === 'stay') return;
|
||||
|
||||
if (action.kind === 'stay-reload') {
|
||||
navigate(
|
||||
{ pathname: location.pathname, search: location.search },
|
||||
{
|
||||
replace: true,
|
||||
state: {
|
||||
...(typeof location.state === 'object' && location.state != null
|
||||
? location.state as Record<string, unknown>
|
||||
: {}),
|
||||
offlineBrowseReloadTs: Date.now(),
|
||||
},
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(action.to, { replace: true });
|
||||
}, [
|
||||
connStatus,
|
||||
ctx.favoritesOfflineBrowse,
|
||||
ctx.localLibraryBrowse,
|
||||
ctx.playerStatsBrowse,
|
||||
ctx.playlistsOfflineBrowse,
|
||||
ctx.hasManualOfflineContent,
|
||||
location.pathname,
|
||||
location.search,
|
||||
location.state,
|
||||
navigate,
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { useOfflineStore } from '@/features/offline/store/offlineStore';
|
||||
import { useConnectionStatus } from '@/hooks/useConnectionStatus';
|
||||
import { usePlayerStatsRecordingEnabled } from '@/features/stats';
|
||||
import { hasOfflineBrowsingContent } from '@/features/offline/utils/favoritesOfflineBrowse';
|
||||
import { useOfflineBrowseActive } from '@/features/offline/utils/offlineBrowseMode';
|
||||
import {
|
||||
buildOfflineBrowseContext,
|
||||
computeOfflineBrowseCapabilities,
|
||||
type OfflineBrowseContext,
|
||||
} from '@/features/offline/utils/offlineBrowseContext';
|
||||
|
||||
/** Single subscription for shell and pages: offline browse mode + capabilities. */
|
||||
export function useOfflineBrowseContext(): OfflineBrowseContext {
|
||||
const active = useOfflineBrowseActive();
|
||||
const serverId = useAuthStore(s => s.activeServerId);
|
||||
const favoritesOfflineEnabled = useAuthStore(s => s.favoritesOfflineEnabled);
|
||||
const offlineAlbums = useOfflineStore(s => s.albums);
|
||||
const playerStats = usePlayerStatsRecordingEnabled();
|
||||
const { status: connStatus } = useConnectionStatus();
|
||||
|
||||
const capabilities = computeOfflineBrowseCapabilities({
|
||||
activeServerId: serverId,
|
||||
favoritesOfflineEnabled,
|
||||
offlineAlbums,
|
||||
playerStats,
|
||||
});
|
||||
|
||||
return buildOfflineBrowseContext({
|
||||
active,
|
||||
serverId,
|
||||
capabilities,
|
||||
connStatus,
|
||||
hasBrowsingContent: hasOfflineBrowsingContent(offlineAlbums),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
/** Bumps when disconnect fork chooses stay-reload ({@link useOfflineAutoNav}). */
|
||||
export function useOfflineBrowseReloadToken(): number | undefined {
|
||||
const location = useLocation();
|
||||
const state = location.state as { offlineBrowseReloadTs?: number } | null;
|
||||
return state?.offlineBrowseReloadTs;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useOfflineBrowseContext } from '@/features/offline/hooks/useOfflineBrowseContext';
|
||||
import {
|
||||
restoreMusicLibraryFiltersAfterOffline,
|
||||
suspendMusicLibraryFiltersForOffline,
|
||||
} from '@/features/offline/utils/offlineLibraryFilterSuspend';
|
||||
|
||||
/** Disable scoped library browse offline; restore the picker value when back online. */
|
||||
export function useOfflineLibraryFilterSuspend(): void {
|
||||
const offlineBrowseActive = useOfflineBrowseContext().active;
|
||||
const prevOfflineRef = useRef<boolean | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const prev = prevOfflineRef.current;
|
||||
prevOfflineRef.current = offlineBrowseActive;
|
||||
|
||||
if (prev === null) {
|
||||
if (offlineBrowseActive) suspendMusicLibraryFiltersForOffline();
|
||||
return;
|
||||
}
|
||||
if (offlineBrowseActive && !prev) {
|
||||
suspendMusicLibraryFiltersForOffline();
|
||||
} else if (!offlineBrowseActive && prev) {
|
||||
restoreMusicLibraryFiltersAfterOffline();
|
||||
}
|
||||
}, [offlineBrowseActive]);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useEffect } from 'react';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { useZipDownloadStore } from '@/features/offline/store/zipDownloadStore';
|
||||
|
||||
/** ZIP download progress events from Rust. */
|
||||
export function useZipDownloadBridge() {
|
||||
useEffect(() => {
|
||||
let unlisten: (() => void) | undefined;
|
||||
listen<{ id: string; bytes: number; total: number | null }>('download:zip:progress', e => {
|
||||
useZipDownloadStore.getState().updateProgress(e.payload.id, e.payload.bytes, e.payload.total);
|
||||
}).then(u => { unlisten = u; });
|
||||
return () => { unlisten?.(); };
|
||||
}, []);
|
||||
}
|
||||
Reference in New Issue
Block a user