diff --git a/src/renderer/router/AppRouter.tsx b/src/renderer/router/app-router.tsx
similarity index 74%
rename from src/renderer/router/AppRouter.tsx
rename to src/renderer/router/app-router.tsx
index 3af5bf442..b71ffbb8c 100644
--- a/src/renderer/router/AppRouter.tsx
+++ b/src/renderer/router/app-router.tsx
@@ -1,15 +1,14 @@
/* eslint-disable sort-keys-fix/sort-keys-fix */
import { Routes, Route } from 'react-router-dom';
+import { LibraryAlbumsRoute } from '@/renderer/features/library/routes/LibraryAlbumsRoute';
+import { LibraryArtistsRoute } from '@/renderer/features/library/routes/LibraryArtistsRoute';
+import { LibraryRoute } from '@/renderer/features/library/routes/LibraryRoute';
+import { AuthOutlet } from '@/renderer/router/auth-outlet';
+import { PrivateOutlet } from '@/renderer/router/private-outlet';
import { LoginRoute } from '../features/auth';
import { DashboardRoute } from '../features/dashboard';
-import { LibraryAlbumsRoute } from '../features/library/routes/LibraryAlbumsRoute';
-import { LibraryArtistsRoute } from '../features/library/routes/LibraryArtistsRoute';
-import { LibraryRoute } from '../features/library/routes/LibraryRoute';
-import { ServersRoute } from '../features/servers';
import { AuthLayout, DefaultLayout } from '../layouts';
-import { AuthOutlet } from './outlets/AuthOutlet';
-import { PrivateOutlet } from './outlets/PrivateOutlet';
-import { AppRoute } from './utils/routes';
+import { AppRoute } from './routes';
export const AppRouter = () => {
return (
@@ -25,7 +24,6 @@ export const AppRouter = () => {
>
}>
} path={AppRoute.HOME} />
- } path={AppRoute.SERVERS} />
>} path={AppRoute.SEARCH} />
} path={AppRoute.LIBRARY} />
diff --git a/src/renderer/router/outlets/AuthOutlet.tsx b/src/renderer/router/auth-outlet.tsx
similarity index 74%
rename from src/renderer/router/outlets/AuthOutlet.tsx
rename to src/renderer/router/auth-outlet.tsx
index ef60b6002..fec09d402 100644
--- a/src/renderer/router/outlets/AuthOutlet.tsx
+++ b/src/renderer/router/auth-outlet.tsx
@@ -1,5 +1,5 @@
import { Navigate, Outlet, useLocation } from 'react-router-dom';
-import { useAuthStore } from '../../store';
+import { useAuthStore } from '../store';
interface AuthOutletProps {
redirectTo: string;
@@ -7,7 +7,7 @@ interface AuthOutletProps {
export const AuthOutlet = ({ redirectTo }: AuthOutletProps) => {
const location = useLocation();
- const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
+ const isAuthenticated = useAuthStore((state) => !!state.accessToken);
if (isAuthenticated) {
return ;
diff --git a/src/renderer/router/outlets/PrivateOutlet.tsx b/src/renderer/router/private-outlet.tsx
similarity index 74%
rename from src/renderer/router/outlets/PrivateOutlet.tsx
rename to src/renderer/router/private-outlet.tsx
index 357ae96e5..ba9edff60 100644
--- a/src/renderer/router/outlets/PrivateOutlet.tsx
+++ b/src/renderer/router/private-outlet.tsx
@@ -1,5 +1,5 @@
import { Navigate, Outlet, useLocation } from 'react-router-dom';
-import { useAuthStore } from '../../store';
+import { useAuthStore } from '@/renderer/store';
interface PrivateOutletProps {
redirectTo: string;
@@ -7,7 +7,7 @@ interface PrivateOutletProps {
export const PrivateOutlet = ({ redirectTo }: PrivateOutletProps) => {
const location = useLocation();
- const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
+ const isAuthenticated = useAuthStore((state) => !!state.accessToken);
if (isAuthenticated) {
return ;
diff --git a/src/renderer/router/utils/routes.ts b/src/renderer/router/routes.ts
similarity index 100%
rename from src/renderer/router/utils/routes.ts
rename to src/renderer/router/routes.ts
diff --git a/src/renderer/store/useAppStore.ts b/src/renderer/store/app.store.ts
similarity index 93%
rename from src/renderer/store/useAppStore.ts
rename to src/renderer/store/app.store.ts
index 9bdb4587c..0747e8b4c 100644
--- a/src/renderer/store/useAppStore.ts
+++ b/src/renderer/store/app.store.ts
@@ -1,6 +1,6 @@
import create from 'zustand';
import { devtools } from 'zustand/middleware';
-import { Platform } from '../../types';
+import { Platform } from '@/types';
export interface AppState {
currentPage: {
diff --git a/src/renderer/store/auth.store.ts b/src/renderer/store/auth.store.ts
new file mode 100644
index 000000000..d4bfa11a2
--- /dev/null
+++ b/src/renderer/store/auth.store.ts
@@ -0,0 +1,124 @@
+import create from 'zustand';
+import { devtools, persist } from 'zustand/middleware';
+import { immer } from 'zustand/middleware/immer';
+import { Server } from '@/renderer/api/types';
+
+export interface AuthState {
+ accessToken: string;
+ currentServer?: Server;
+ permissions: {
+ isAdmin: boolean;
+ };
+ refreshToken: string;
+ serverCredentials: {
+ enabled: boolean;
+ id: string;
+ serverId: string;
+ token: string;
+ username: string;
+ }[];
+ serverKey: string;
+ serverUrl: string;
+}
+
+export interface AuthSlice extends AuthState {
+ addServerCredential: (options: {
+ enabled: boolean;
+ id: string;
+ serverId: string;
+ token: string;
+ username: string;
+ }) => void;
+ deleteServerCredential: (options: { id: string }) => void;
+ disableServerCredential: (options: { id: string }) => void;
+ enableServerCredential: (options: { id: string }) => void;
+ login: (auth: Partial) => void;
+ logout: () => void;
+ setCurrentServer: (server: Server) => void;
+}
+
+const persistedState = JSON.parse(
+ localStorage.getItem('authentication') || '{}'
+);
+
+export const useAuthStore = create()(
+ persist(
+ devtools(
+ immer((set) => ({
+ accessToken: '',
+ addServerCredential: (options) => {
+ set((state) => {
+ state.serverCredentials = state.serverCredentials.filter(
+ (c) => c.username !== options.username
+ );
+ state.serverCredentials.push(options);
+ });
+ },
+ currentServer: undefined,
+ deleteServerCredential: (options) => {
+ set((state) => {
+ state.serverCredentials = state.serverCredentials.filter(
+ (credential) => credential.id !== options.id
+ );
+ });
+ },
+ disableServerCredential: (options) => {
+ set((state) => {
+ state.serverCredentials = state.serverCredentials.map(
+ (credential) => {
+ if (credential.id === options.id) {
+ credential.enabled = false;
+ }
+ return credential;
+ }
+ );
+ });
+ },
+ enableServerCredential: (options) => {
+ set((state) => {
+ state.serverCredentials = state.serverCredentials.map(
+ (credential) => {
+ if (credential.id === options.id) {
+ credential.enabled = true;
+ }
+ return credential;
+ }
+ );
+ });
+ },
+ login: (auth: Partial) => {
+ return set({ ...auth });
+ },
+ logout: () => {
+ return set({
+ accessToken: undefined,
+ permissions: { isAdmin: false },
+ refreshToken: undefined,
+ });
+ },
+ permissions: {
+ isAdmin: false,
+ },
+ refreshToken: '',
+ serverCredentials: [],
+ serverKey: '',
+ serverPermissions: '',
+ serverUrl: '',
+ setCurrentServer: (server: Server) => {
+ const prev = JSON.parse(
+ localStorage.getItem('authentication') || '{}'
+ );
+ localStorage.setItem(
+ 'authentication',
+ JSON.stringify({
+ ...prev,
+ state: { ...prev.state, currentServer: server },
+ })
+ );
+ return set({ currentServer: server });
+ },
+ }))
+ ),
+ { name: 'authentication' }
+ )
+);
diff --git a/src/renderer/store/index.ts b/src/renderer/store/index.ts
index e84f5a7b4..4bcde8b46 100644
--- a/src/renderer/store/index.ts
+++ b/src/renderer/store/index.ts
@@ -1,2 +1,3 @@
-export * from './useAuthStore';
-export * from './usePlayerStore';
+export * from './auth.store';
+export * from './player.store';
+export * from './app.store';
diff --git a/src/renderer/store/player.store.ts b/src/renderer/store/player.store.ts
new file mode 100644
index 000000000..b9ac02769
--- /dev/null
+++ b/src/renderer/store/player.store.ts
@@ -0,0 +1,280 @@
+/* eslint-disable prefer-destructuring */
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import produce from 'immer';
+import map from 'lodash/map';
+import { nanoid } from 'nanoid/non-secure';
+import create from 'zustand';
+import { devtools, persist } from 'zustand/middleware';
+import { Song } from '@/renderer/api/types';
+import {
+ Play,
+ CrossfadeStyle,
+ PlaybackStyle,
+ PlaybackType,
+ PlayerRepeat,
+ PlayerStatus,
+ UniqueId,
+} from '../../types';
+
+type QueueSong = Song & UniqueId;
+
+export interface PlayerState {
+ current: {
+ index: number;
+ player: 1 | 2;
+ song: QueueSong;
+ status: PlayerStatus;
+ time: number;
+ };
+ queue: {
+ default: QueueSong[];
+ previousNode: QueueSong;
+ shuffled: QueueSong[];
+ sorted: QueueSong[];
+ };
+ settings: {
+ crossfadeDuration: number;
+ crossfadeStyle: CrossfadeStyle;
+ muted: boolean;
+ repeat: PlayerRepeat;
+ shuffle: boolean;
+ style: PlaybackStyle;
+ type: PlaybackType;
+ volume: number;
+ };
+}
+
+export interface PlayerData {
+ current: {
+ index: number;
+ player: 1 | 2;
+ song: QueueSong;
+ status: PlayerStatus;
+ };
+ player1: QueueSong;
+ player2: QueueSong;
+ queue: QueueData;
+}
+
+export interface QueueData {
+ current: QueueSong;
+ next: QueueSong;
+ previous: QueueSong;
+}
+
+export interface PlayerSlice extends PlayerState {
+ addToQueue: (songs: Song[], type: Play) => PlayerData;
+ autoNext: () => PlayerData;
+ getPlayerData: () => PlayerData;
+ getQueueData: () => QueueData;
+ next: () => PlayerData;
+ pause: () => void;
+ play: () => void;
+ player1: () => QueueSong;
+ player2: () => QueueSong;
+ prev: () => PlayerData;
+ setCurrentIndex: (index: number) => PlayerData;
+ setCurrentTime: (time: number) => void;
+ setSettings: (settings: Partial) => void;
+}
+
+export const usePlayerStore = create()(
+ persist(
+ devtools((set, get) => ({
+ addToQueue: (songs, type) => {
+ const queueSongs = map(songs, (song) => ({
+ ...song,
+ uniqueId: nanoid(),
+ }));
+
+ if (type === Play.NOW) {
+ set(
+ produce((state) => {
+ state.queue.default = queueSongs;
+ state.current.time = 0;
+ state.current.player = 1;
+ state.current.index = 0;
+ state.current.song = queueSongs[0];
+ })
+ );
+ } else if (type === Play.LAST) {
+ set(
+ produce((state) => {
+ state.queue.default = [...get().queue.default, ...queueSongs];
+ })
+ );
+ } else if (type === Play.NEXT) {
+ const queue = get().queue.default;
+ const currentIndex = get().current.index;
+
+ set(
+ produce((state) => {
+ state.queue.default = [
+ ...queue.slice(0, currentIndex + 1),
+ ...queueSongs,
+ ...queue.slice(currentIndex + 1),
+ ];
+ })
+ );
+ }
+
+ return get().getPlayerData();
+ },
+ autoNext: () => {
+ set(
+ produce((state) => {
+ state.current.time = 0;
+ state.current.index += 1;
+ state.current.player = state.current.player === 1 ? 2 : 1;
+ state.current.song = state.queue.default[state.current.index];
+ state.queue.previousNode = get().current.song;
+ })
+ );
+
+ return get().getPlayerData();
+ },
+ current: {
+ index: 0,
+ player: 1,
+ song: {} as QueueSong,
+ status: PlayerStatus.PAUSED,
+ time: 0,
+ },
+ getPlayerData: () => {
+ const queue = get().queue.default;
+ const currentPlayer = get().current.player;
+
+ const player1 =
+ currentPlayer === 1
+ ? queue[get().current.index]
+ : queue[get().current.index + 1];
+
+ const player2 =
+ currentPlayer === 1
+ ? queue[get().current.index + 1]
+ : queue[get().current.index];
+
+ return {
+ current: {
+ index: get().current.index,
+ player: get().current.player,
+ song: get().current.song,
+ status: get().current.status,
+ },
+ player1,
+ player2,
+ queue: {
+ current: queue[get().current.index],
+ next: queue[get().current.index + 1],
+ previous: queue[get().current.index - 1],
+ },
+ };
+ },
+ getQueueData: () => {
+ const queue = get().queue.default;
+ return {
+ current: queue[get().current.index],
+ next: queue[get().current.index + 1],
+ previous: queue[get().current.index - 1],
+ };
+ },
+ next: () => {
+ set(
+ produce((state) => {
+ state.current.time = 0;
+ state.current.index += 1;
+ state.current.player = 1;
+ state.current.song = state.queue.default[state.current.index];
+ state.queue.previousNode = get().current.song;
+ })
+ );
+
+ return get().getPlayerData();
+ },
+ pause: () => {
+ set(
+ produce((state) => {
+ state.current.status = PlayerStatus.PAUSED;
+ })
+ );
+ },
+ play: () => {
+ set(
+ produce((state) => {
+ state.current.status = PlayerStatus.PLAYING;
+ })
+ );
+ },
+ player1: () => {
+ return get().getPlayerData().player1;
+ },
+ player2: () => {
+ return get().getPlayerData().player2;
+ },
+ prev: () => {
+ set(
+ produce((state) => {
+ state.current.time = 0;
+ state.current.index =
+ state.current.index - 1 < 0 ? 0 : state.current.index - 1;
+ state.current.player = 1;
+ state.current.song = state.queue.default[state.current.index];
+ state.queue.previousNode = get().current.song;
+ })
+ );
+
+ return get().getPlayerData();
+ },
+ queue: {
+ default: [],
+ previousNode: {} as QueueSong,
+ shuffled: [],
+ sorted: [],
+ },
+ setCurrentIndex: (index) => {
+ set(
+ produce((state) => {
+ state.current.time = 0;
+ state.current.index = index;
+ state.current.player = 1;
+ state.current.song = state.queue.default[index];
+ state.queue.previousNode = get().current.song;
+ })
+ );
+
+ return get().getPlayerData();
+ },
+ setCurrentTime: (time) => {
+ set(
+ produce((state) => {
+ state.current.time = time;
+ })
+ );
+ },
+ setSettings: (settings) => {
+ set(
+ produce((state) => {
+ state.settings = { ...get().settings, ...settings };
+ })
+ );
+
+ // try {
+ // setLocalStorageSettings('player', get().settings);
+ // } catch (err) {
+ // console.log('none');
+ // }
+ },
+ settings: {
+ crossfadeDuration: 5,
+ crossfadeStyle: CrossfadeStyle.EQUALPOWER,
+ muted: false,
+ repeat: PlayerRepeat.NONE,
+ shuffle: false,
+ style: PlaybackStyle.GAPLESS,
+ type: PlaybackType.LOCAL,
+ volume: 50,
+ },
+ })),
+ { name: 'player' }
+ )
+);
diff --git a/src/renderer/store/useAuthStore.ts b/src/renderer/store/useAuthStore.ts
deleted file mode 100644
index 80013dad2..000000000
--- a/src/renderer/store/useAuthStore.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import create from 'zustand';
-import { devtools } from 'zustand/middleware';
-import { immer } from 'zustand/middleware/immer';
-
-export interface AuthState {
- accessToken: string;
- isAuthenticated: boolean;
- key: string;
- refreshToken: string;
- serverUrl: string;
-}
-
-export interface AuthSlice extends AuthState {
- login: (auth: Partial) => void;
- logout: () => void;
-}
-
-const persistedAuthState = JSON.parse(
- localStorage.getItem('authentication') || '{}'
-);
-
-export const useAuthStore = create()(
- devtools(
- immer((set) => ({
- accessToken: persistedAuthState.accessToken,
- isAuthenticated: persistedAuthState.isAuthenticated,
- key: persistedAuthState.key,
- login: (auth: Partial) => {
- return set({ isAuthenticated: true, ...auth });
- },
- logout: () => {
- return set({ isAuthenticated: false });
- },
- refreshToken: persistedAuthState.refreshToken,
- serverUrl: persistedAuthState.serverUrl,
- }))
- )
-);
diff --git a/src/renderer/store/usePlayerStore.ts b/src/renderer/store/usePlayerStore.ts
deleted file mode 100644
index ae847d27a..000000000
--- a/src/renderer/store/usePlayerStore.ts
+++ /dev/null
@@ -1,240 +0,0 @@
-/* eslint-disable prefer-destructuring */
-/* eslint-disable @typescript-eslint/no-unused-vars */
-import produce from 'immer';
-import create from 'zustand';
-import { devtools } from 'zustand/middleware';
-import {
- Play,
- CrossfadeStyle,
- PlaybackStyle,
- PlaybackType,
- PlayerRepeat,
- PlayerStatus,
- Song,
-} from '../../types';
-import { setLocalStorageSettings } from '../utils';
-
-export interface PlayerState {
- current: {
- index: number;
- player: 1 | 2;
- song: Song;
- status: PlayerStatus;
- time: number;
- };
- queue: {
- default: Song[];
- shuffled: Song[];
- sorted: Song[];
- };
- settings: {
- crossfadeDuration: number;
- crossfadeStyle: CrossfadeStyle;
- muted: boolean;
- repeat: PlayerRepeat;
- shuffle: boolean;
- style: PlaybackStyle;
- type: PlaybackType;
- volume: number;
- };
-}
-
-export interface PlayerData {
- current: {
- index: number;
- player: 1 | 2;
- song: Song;
- status: PlayerStatus;
- time: number;
- };
- player1: Song;
- player2: Song;
- queue: {
- current: Song;
- next: Song;
- previous: Song;
- };
-}
-
-export interface PlayerSlice extends PlayerState {
- addToQueue: (songs: Song[], type: Play) => PlayerData;
- autoNext: () => PlayerData;
- getPlayerData: () => PlayerData;
- next: () => PlayerData;
- pause: () => void;
- play: () => void;
- player1: () => Song;
- player2: () => Song;
- prev: () => PlayerData;
- setCurrentTime: (time: number) => void;
- setSettings: (settings: Partial) => void;
-}
-
-export const usePlayerStore = create()(
- devtools((set, get) => ({
- addToQueue: (songs, type) => {
- if (type === Play.NOW) {
- set(
- produce((state) => {
- state.queue.default = songs;
- state.current.time = 0;
- state.current.player = 1;
- state.current.index = 0;
- state.current.song = songs[0];
- })
- );
- } else if (type === Play.LAST) {
- set(
- produce((state) => {
- state.queue.default = [...get().queue.default, ...songs];
- })
- );
- } else if (type === Play.NEXT) {
- const queue = get().queue.default;
- const currentIndex = get().current.index;
-
- set(
- produce((state) => {
- state.queue.default = [
- ...queue.slice(0, currentIndex + 1),
- ...songs,
- ...queue.slice(currentIndex + 1),
- ];
- })
- );
- }
-
- return get().getPlayerData();
- },
- autoNext: () => {
- set(
- produce((state) => {
- state.current.time = 0;
- state.current.index += 1;
- state.current.player = state.current.player === 1 ? 2 : 1;
- state.current.song = state.queue.default[state.current.index];
- })
- );
-
- return get().getPlayerData();
- },
- current: {
- index: 0,
- player: 1,
- song: {} as Song,
- status: PlayerStatus.PAUSED,
- time: 0,
- },
- getPlayerData: () => {
- const queue = get().queue.default;
- const currentPlayer = get().current.player;
-
- const player1 =
- currentPlayer === 1
- ? queue[get().current.index]
- : queue[get().current.index + 1];
-
- const player2 =
- currentPlayer === 1
- ? queue[get().current.index + 1]
- : queue[get().current.index];
-
- return {
- current: {
- index: get().current.index,
- player: get().current.player,
- song: get().current.song,
- status: get().current.status,
- time: get().current.time,
- },
- player1,
- player2,
- queue: {
- current: queue[get().current.index],
- next: queue[get().current.index + 1],
- previous: queue[get().current.index - 1],
- },
- };
- },
- next: () => {
- set(
- produce((state) => {
- state.current.time = 0;
- state.current.index += 1;
- state.current.player = 1;
- state.current.song = state.queue.default[state.current.index];
- })
- );
-
- return get().getPlayerData();
- },
- pause: () => {
- set(
- produce((state) => {
- state.current.status = PlayerStatus.PAUSED;
- })
- );
- },
- play: () => {
- set(
- produce((state) => {
- state.current.status = PlayerStatus.PLAYING;
- })
- );
- },
- player1: () => {
- return get().getPlayerData().player1;
- },
- player2: () => {
- return get().getPlayerData().player2;
- },
- prev: () => {
- set(
- produce((state) => {
- state.current.time = 0;
- state.current.index =
- state.current.index - 1 < 0 ? 0 : state.current.index - 1;
- state.current.player = 1;
- state.current.song = state.queue.default[state.current.index];
- })
- );
-
- return get().getPlayerData();
- },
- queue: {
- default: [],
- shuffled: [],
- sorted: [],
- },
- setCurrentTime: (time) => {
- set(
- produce((state) => {
- state.current.time = time;
- })
- );
- },
- setSettings: (settings) => {
- set(
- produce((state) => {
- state.settings = { ...get().settings, ...settings };
- })
- );
-
- try {
- setLocalStorageSettings('player', get().settings);
- } catch (err) {
- console.log('none');
- }
- },
- settings: {
- crossfadeDuration: 5,
- crossfadeStyle: CrossfadeStyle.EQUALPOWER,
- muted: false,
- repeat: PlayerRepeat.NONE,
- shuffle: false,
- style: PlaybackStyle.GAPLESS,
- type: PlaybackType.LOCAL,
- volume: 50,
- },
- }))
-);