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, - }, - })) -);