Environment upgrade & hot-cache playback (#463)

* chore: upgrade dependencies and migrate playback to rodio 0.22

Bump npm and Rust crates; adapt symphonia decoding, ringbuf 0.5, lofty tags,
and discord-rich-presence usage. Use native rodio Player/MixerDeviceSink and
cpal device descriptions; drop the unused cpal patch. Align Vite 8 build
targets and chunking; remove redundant dynamic imports and fix hot-cache debug
logging imports.

* perf(build): lazy-load routes and restore default chunk warnings

Lazy-load all routed pages with React.lazy to shrink the main bundle; wrap root
Routes in Suspense for lazy Login. Drop chunkSizeWarningLimit override so Vite
uses the default 500 kB threshold.

* fix(windows): tray double-click without spurious menu; clean unused import

Disable tray menu on left mouse-up on Windows so a double-click to hide the
main window does not immediately reopen the context menu (tray-icon default
menu_on_left_click). Gate std::fs in app_api/core behind cfg(linux) for
/proc-only code so Windows builds stay warning-free.

* fix(sidebar): preserve new-releases read state under storage cap

When merging seen album ids, keep the current newest sample first so the
500-id localStorage limit does not truncate freshly marked reads and bring
back the unread badge.

* fix(audio): hot-cache replay, analysis no-op skips, playback source UI

Retain stream_completed_cache across audio_stop so end-of-queue replay can
use RAM promote or disk hot file instead of re-ranging HTTP.

Add cpu_seed_redundant_for_track gate before file/bytes seeds and local-file
spawn; emit analysis:waveform-updated only on Upserted. Ranged/legacy promote
checks generation after await before filling the slot.

Frontend: promote on same-track and cold resume; set currentPlaybackSource on
resume, queue undo restore, and gapless track switch so cache/stream icons stay
accurate. Import tauri::Manager for try_state in audio_play.

* fix(ts): narrow activeServerId for hot-cache promote calls

promoteCompletedStreamToHotCache expects a string; bind non-null server ids
in repeat-one, playTrack prev/same-track, and cold resume paths so tauri
production build (tsc) succeeds.

* fix(player): handle same-track hot-cache promote promise chain

Add .catch for promoteCompletedStreamToHotCache → runPlayTrackBody so sync
throws and unexpected rejections do not surface as unhandled in DevTools;
reset defer-hot-cache prefetch and isPlaying on failure.

* chore(nix): sync npmDepsHash with package-lock.json

* chore(release): finalize 1.46.0 CHANGELOG with PR #463 links

Document the release with full GitHub PR #463 on every subsection so
entries stay attributable if sections are reordered. Fix ContextMenu
lines where dynamic imports were accidentally merged onto one line.

* docs(contributors): credit cucadmuh for #463
This commit is contained in:
cucadmuh
2026-05-05 22:00:29 +03:00
committed by GitHub
parent 54e774ef24
commit 8d8c1aa8a3
29 changed files with 2381 additions and 2511 deletions
+46 -45
View File
@@ -15,38 +15,39 @@ import { WindowVisibilityProvider } from './hooks/useWindowVisibility';
import LiveSearch from './components/LiveSearch';
import NowPlayingDropdown from './components/NowPlayingDropdown';
import QueuePanel from './components/QueuePanel';
// Eager — main browsing flow, loaded on first paint
import Home from './pages/Home';
import Albums from './pages/Albums';
import Artists from './pages/Artists';
import ArtistDetail from './pages/ArtistDetail';
import NewReleases from './pages/NewReleases';
import Favorites from './pages/Favorites';
import RandomMix from './pages/RandomMix';
import RandomLanding from './pages/RandomLanding';
import Login from './pages/Login';
import AlbumDetail from './pages/AlbumDetail';
import MostPlayed from './pages/MostPlayed';
import RandomAlbums from './pages/RandomAlbums';
import LuckyMixPage from './pages/LuckyMix';
import SearchResults from './pages/SearchResults';
import Playlists from './pages/Playlists';
import PlaylistDetail from './pages/PlaylistDetail';
import NowPlayingPage from './pages/NowPlaying';
// Lazy — visited rarely or on-demand. Each becomes its own chunk so the
// initial bundle stays smaller and these pages don't block first paint.
const Settings = lazy(() => import('./pages/Settings'));
const Statistics = lazy(() => import('./pages/Statistics'));
const Help = lazy(() => import('./pages/Help'));
const WhatsNew = lazy(() => import('./pages/WhatsNew'));
const DeviceSync = lazy(() => import('./pages/DeviceSync'));
// Route-level lazy loading: keeps the non-page graph (shell, player, stores) in
// the entry chunk; each page is fetched when its route is first visited.
const Home = lazy(() => import('./pages/Home'));
const Albums = lazy(() => import('./pages/Albums'));
const Artists = lazy(() => import('./pages/Artists'));
const ArtistDetail = lazy(() => import('./pages/ArtistDetail'));
const NewReleases = lazy(() => import('./pages/NewReleases'));
const Favorites = lazy(() => import('./pages/Favorites'));
const RandomMix = lazy(() => import('./pages/RandomMix'));
const RandomLanding = lazy(() => import('./pages/RandomLanding'));
const Login = lazy(() => import('./pages/Login'));
const AlbumDetail = lazy(() => import('./pages/AlbumDetail'));
const MostPlayed = lazy(() => import('./pages/MostPlayed'));
const RandomAlbums = lazy(() => import('./pages/RandomAlbums'));
const LuckyMixPage = lazy(() => import('./pages/LuckyMix'));
const SearchResults = lazy(() => import('./pages/SearchResults'));
const Playlists = lazy(() => import('./pages/Playlists'));
const PlaylistDetail = lazy(() => import('./pages/PlaylistDetail'));
const NowPlayingPage = lazy(() => import('./pages/NowPlaying'));
const Tracks = lazy(() => import('./pages/Tracks'));
const Settings = lazy(() => import('./pages/Settings'));
const Statistics = lazy(() => import('./pages/Statistics'));
const Help = lazy(() => import('./pages/Help'));
const WhatsNew = lazy(() => import('./pages/WhatsNew'));
const DeviceSync = lazy(() => import('./pages/DeviceSync'));
const OfflineLibrary = lazy(() => import('./pages/OfflineLibrary'));
const LabelAlbums = lazy(() => import('./pages/LabelAlbums'));
const LabelAlbums = lazy(() => import('./pages/LabelAlbums'));
const AdvancedSearch = lazy(() => import('./pages/AdvancedSearch'));
const FolderBrowser = lazy(() => import('./pages/FolderBrowser'));
const InternetRadio = lazy(() => import('./pages/InternetRadio'));
const Tracks = lazy(() => import('./pages/Tracks'));
const FolderBrowser = lazy(() => import('./pages/FolderBrowser'));
const InternetRadio = lazy(() => import('./pages/InternetRadio'));
const Genres = lazy(() => import('./pages/Genres'));
const GenreDetail = lazy(() => import('./pages/GenreDetail'));
import MiniPlayer from './components/MiniPlayer';
import { initMiniPlayerBridgeOnMain } from './utils/miniPlayerBridge';
import FullscreenPlayer from './components/FullscreenPlayer';
@@ -63,8 +64,6 @@ import { APP_MAIN_SCROLL_VIEWPORT_ID } from './constants/appScroll';
import ConnectionIndicator from './components/ConnectionIndicator';
import LastfmIndicator from './components/LastfmIndicator';
import OfflineBanner from './components/OfflineBanner';
import Genres from './pages/Genres';
import GenreDetail from './pages/GenreDetail';
import ExportPickerModal from './components/ExportPickerModal';
import AppUpdater from './components/AppUpdater';
import TitleBar from './components/TitleBar';
@@ -1316,7 +1315,7 @@ export default function App() {
else if (e.key === 'psysonic_font') useFontStore.persist.rehydrate();
else if (e.key === 'psysonic_keybindings') useKeybindingsStore.persist.rehydrate();
else if (e.key === 'psysonic_language' && e.newValue) {
import('./i18n').then(m => m.default.changeLanguage(e.newValue!));
i18n.changeLanguage(e.newValue);
}
};
window.addEventListener('storage', onStorage);
@@ -1402,19 +1401,21 @@ export default function App() {
<BrowserRouter>
<PasteClipboardHandler />
<TauriEventBridge />
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/*"
element={
<RequireAuth>
<DragDropProvider>
<AppShell />
</DragDropProvider>
</RequireAuth>
}
/>
</Routes>
<Suspense fallback={null}>
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/*"
element={
<RequireAuth>
<DragDropProvider>
<AppShell />
</DragDropProvider>
</RequireAuth>
}
/>
</Routes>
</Suspense>
{exportPickerOpen && <ExportPickerModal onConfirm={handleExport} onClose={() => setExportPickerOpen(false)} />}
<ZipDownloadOverlay />
</BrowserRouter>