Project started 🥂
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { api } from '../api';
|
||||
import authReducer from './slices/auth';
|
||||
import playerReducer from './slices/player';
|
||||
import queueReducer from './slices/queue';
|
||||
import uiReducer from './slices/ui';
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
[api.reducerPath]: api.reducer,
|
||||
auth: authReducer,
|
||||
player: playerReducer,
|
||||
queue: queueReducer,
|
||||
ui: uiReducer,
|
||||
},
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().concat(api.middleware),
|
||||
});
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
@@ -0,0 +1,65 @@
|
||||
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
||||
import type { User } from '../../api/types';
|
||||
|
||||
interface AuthState {
|
||||
accessToken: string | null;
|
||||
refreshToken: string | null;
|
||||
expiresAt: number | null;
|
||||
user: User | null;
|
||||
}
|
||||
|
||||
const loadPersistedAuth = (): Partial<AuthState> => {
|
||||
try {
|
||||
const raw = localStorage.getItem('mcma_auth');
|
||||
return raw ? (JSON.parse(raw) as Partial<AuthState>) : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const persisted = loadPersistedAuth();
|
||||
|
||||
const initialState: AuthState = {
|
||||
accessToken: persisted.accessToken ?? null,
|
||||
refreshToken: persisted.refreshToken ?? null,
|
||||
expiresAt: persisted.expiresAt ?? null,
|
||||
user: persisted.user ?? null,
|
||||
};
|
||||
|
||||
export const authSlice = createSlice({
|
||||
name: 'auth',
|
||||
initialState,
|
||||
reducers: {
|
||||
setTokens(state, action: PayloadAction<{ accessToken: string; refreshToken: string; expiresIn: number }>) {
|
||||
state.accessToken = action.payload.accessToken;
|
||||
state.refreshToken = action.payload.refreshToken;
|
||||
state.expiresAt = Date.now() + action.payload.expiresIn * 1000;
|
||||
persistAuth(state);
|
||||
},
|
||||
setUser(state, action: PayloadAction<User>) {
|
||||
state.user = action.payload;
|
||||
persistAuth(state);
|
||||
},
|
||||
logout(state) {
|
||||
state.accessToken = null;
|
||||
state.refreshToken = null;
|
||||
state.expiresAt = null;
|
||||
state.user = null;
|
||||
localStorage.removeItem('mcma_auth');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function persistAuth(state: AuthState): void {
|
||||
try {
|
||||
localStorage.setItem('mcma_auth', JSON.stringify({
|
||||
accessToken: state.accessToken,
|
||||
refreshToken: state.refreshToken,
|
||||
expiresAt: state.expiresAt,
|
||||
user: state.user,
|
||||
}));
|
||||
} catch { /* storage unavailable */ }
|
||||
}
|
||||
|
||||
export const { setTokens, setUser, logout } = authSlice.actions;
|
||||
export default authSlice.reducer;
|
||||
@@ -0,0 +1,61 @@
|
||||
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
type RepeatMode = 'none' | 'one' | 'all';
|
||||
|
||||
interface PlayerState {
|
||||
currentTrackId: string | null;
|
||||
isPlaying: boolean;
|
||||
position: number;
|
||||
duration: number;
|
||||
volume: number;
|
||||
muted: boolean;
|
||||
repeat: RepeatMode;
|
||||
shuffle: boolean;
|
||||
isNowPlayingOpen: boolean;
|
||||
isQueueOpen: boolean;
|
||||
}
|
||||
|
||||
const initialState: PlayerState = {
|
||||
currentTrackId: null,
|
||||
isPlaying: false,
|
||||
position: 0,
|
||||
duration: 0,
|
||||
volume: 0.8,
|
||||
muted: false,
|
||||
repeat: 'none',
|
||||
shuffle: false,
|
||||
isNowPlayingOpen: false,
|
||||
isQueueOpen: false,
|
||||
};
|
||||
|
||||
export const playerSlice = createSlice({
|
||||
name: 'player',
|
||||
initialState,
|
||||
reducers: {
|
||||
play(state, action: PayloadAction<string>) {
|
||||
state.currentTrackId = action.payload;
|
||||
state.isPlaying = true;
|
||||
state.position = 0;
|
||||
},
|
||||
pause(state) { state.isPlaying = false; },
|
||||
resume(state) { state.isPlaying = true; },
|
||||
stop(state) { state.isPlaying = false; state.currentTrackId = null; state.position = 0; },
|
||||
setPosition(state, action: PayloadAction<number>) { state.position = action.payload; },
|
||||
setDuration(state, action: PayloadAction<number>) { state.duration = action.payload; },
|
||||
setVolume(state, action: PayloadAction<number>) { state.volume = action.payload; },
|
||||
toggleMute(state) { state.muted = !state.muted; },
|
||||
setRepeat(state, action: PayloadAction<RepeatMode>) { state.repeat = action.payload; },
|
||||
toggleShuffle(state) { state.shuffle = !state.shuffle; },
|
||||
toggleNowPlaying(state) { state.isNowPlayingOpen = !state.isNowPlayingOpen; },
|
||||
toggleQueue(state) { state.isQueueOpen = !state.isQueueOpen; },
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
play, pause, resume, stop,
|
||||
setPosition, setDuration,
|
||||
setVolume, toggleMute,
|
||||
setRepeat, toggleShuffle,
|
||||
toggleNowPlaying, toggleQueue,
|
||||
} = playerSlice.actions;
|
||||
export default playerSlice.reducer;
|
||||
@@ -0,0 +1,79 @@
|
||||
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
type QueueSource = 'manual' | 'album' | 'playlist' | 'artist' | 'search';
|
||||
|
||||
interface QueueEntry {
|
||||
trackId: string;
|
||||
title: string;
|
||||
artistName: string;
|
||||
albumTitle: string;
|
||||
durationMs: number;
|
||||
albumArtUrl?: string;
|
||||
}
|
||||
|
||||
interface QueueState {
|
||||
entries: QueueEntry[];
|
||||
currentIndex: number;
|
||||
source: QueueSource;
|
||||
sourceId: string | null;
|
||||
sourceName: string | null;
|
||||
}
|
||||
|
||||
const initialState: QueueState = {
|
||||
entries: [],
|
||||
currentIndex: -1,
|
||||
source: 'manual',
|
||||
sourceId: null,
|
||||
sourceName: null,
|
||||
};
|
||||
|
||||
export const queueSlice = createSlice({
|
||||
name: 'queue',
|
||||
initialState,
|
||||
reducers: {
|
||||
setQueue(state, action: PayloadAction<{ entries: QueueEntry[]; startIndex?: number; source: QueueSource; sourceId?: string; sourceName?: string }>) {
|
||||
state.entries = action.payload.entries;
|
||||
state.currentIndex = action.payload.startIndex ?? 0;
|
||||
state.source = action.payload.source;
|
||||
state.sourceId = action.payload.sourceId ?? null;
|
||||
state.sourceName = action.payload.sourceName ?? null;
|
||||
},
|
||||
addToQueue(state, action: PayloadAction<QueueEntry>) {
|
||||
state.entries.push(action.payload);
|
||||
},
|
||||
addNextInQueue(state, action: PayloadAction<QueueEntry>) {
|
||||
state.entries.splice(state.currentIndex + 1, 0, action.payload);
|
||||
},
|
||||
removeFromQueue(state, action: PayloadAction<number>) {
|
||||
state.entries.splice(action.payload, 1);
|
||||
if (action.payload < state.currentIndex) state.currentIndex--;
|
||||
},
|
||||
moveInQueue(state, action: PayloadAction<{ from: number; to: number }>) {
|
||||
const { from, to } = action.payload;
|
||||
const [entry] = state.entries.splice(from, 1);
|
||||
state.entries.splice(to, 0, entry);
|
||||
if (state.currentIndex === from) state.currentIndex = to;
|
||||
else if (from < state.currentIndex && to >= state.currentIndex) state.currentIndex--;
|
||||
else if (from > state.currentIndex && to <= state.currentIndex) state.currentIndex++;
|
||||
},
|
||||
goToIndex(state, action: PayloadAction<number>) {
|
||||
state.currentIndex = action.payload;
|
||||
},
|
||||
nextTrack(state) {
|
||||
if (state.currentIndex < state.entries.length - 1) state.currentIndex++;
|
||||
},
|
||||
prevTrack(state) {
|
||||
if (state.currentIndex > 0) state.currentIndex--;
|
||||
},
|
||||
clearQueue(state) {
|
||||
state.entries = [];
|
||||
state.currentIndex = -1;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
setQueue, addToQueue, addNextInQueue, removeFromQueue,
|
||||
moveInQueue, goToIndex, nextTrack, prevTrack, clearQueue,
|
||||
} = queueSlice.actions;
|
||||
export default queueSlice.reducer;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
interface UiState {
|
||||
sidebarCollapsed: boolean;
|
||||
activeModal: string | null;
|
||||
activeTrackContextMenuId: string | null;
|
||||
}
|
||||
|
||||
const initialState: UiState = {
|
||||
sidebarCollapsed: false,
|
||||
activeModal: null,
|
||||
activeTrackContextMenuId: null,
|
||||
};
|
||||
|
||||
export const uiSlice = createSlice({
|
||||
name: 'ui',
|
||||
initialState,
|
||||
reducers: {
|
||||
toggleSidebar(state) { state.sidebarCollapsed = !state.sidebarCollapsed; },
|
||||
setSidebarCollapsed(state, action: PayloadAction<boolean>) { state.sidebarCollapsed = action.payload; },
|
||||
openModal(state, action: PayloadAction<string>) { state.activeModal = action.payload; },
|
||||
closeModal(state) { state.activeModal = null; },
|
||||
setActiveContextMenu(state, action: PayloadAction<string | null>) { state.activeTrackContextMenuId = action.payload; },
|
||||
},
|
||||
});
|
||||
|
||||
export const { toggleSidebar, setSidebarCollapsed, openModal, closeModal, setActiveContextMenu } = uiSlice.actions;
|
||||
export default uiSlice.reducer;
|
||||
Reference in New Issue
Block a user