From a44ad66d466dec1a115e28501805eb677a6a2de5 Mon Sep 17 00:00:00 2001 From: Lyall Date: Thu, 31 Jul 2025 16:12:03 +0100 Subject: [PATCH 1/3] add private mode toggle to app menu --- src/i18n/locales/en.json | 2 ++ .../features/discord-rpc/use-discord-rpc.ts | 10 +++++--- .../features/player/hooks/use-scrobble.ts | 17 +++++++------ .../features/titlebar/components/app-menu.tsx | 25 +++++++++++++++++-- src/renderer/store/app.store.ts | 8 ++++++ src/shared/components/icon/icon.tsx | 2 ++ 6 files changed, 51 insertions(+), 13 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index f0709093..414f277a 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -321,6 +321,8 @@ "goBack": "go back", "goForward": "go forward", "manageServers": "manage servers", + "privateModeOff": "turn private mode off", + "privateModeOn": "turn private mode on", "openBrowserDevtools": "open browser devtools", "quit": "$t(common.quit)", "selectServer": "select server", diff --git a/src/renderer/features/discord-rpc/use-discord-rpc.ts b/src/renderer/features/discord-rpc/use-discord-rpc.ts index a0586159..96ec67d7 100644 --- a/src/renderer/features/discord-rpc/use-discord-rpc.ts +++ b/src/renderer/features/discord-rpc/use-discord-rpc.ts @@ -5,6 +5,7 @@ import { useCallback, useEffect, useState } from 'react'; import { controller } from '/@/renderer/api/controller'; import { getServerById, + useAppStore, useDiscordSetttings, useGeneralSettings, usePlayerStore, @@ -17,6 +18,7 @@ const discordRpc = isElectron() ? window.api.discordRpc : null; export const useDiscordRpc = () => { const discordSettings = useDiscordSetttings(); const generalSettings = useGeneralSettings(); + const { privateMode } = useAppStore(); const [lastUniqueId, setlastUniqueId] = useState(''); const setActivity = useCallback( @@ -139,15 +141,15 @@ export const useDiscordRpc = () => { ); useEffect(() => { - if (!discordSettings.enabled) return discordRpc?.quit(); + if (!discordSettings.enabled || privateMode) return discordRpc?.quit(); return () => { discordRpc?.quit(); }; - }, [discordSettings.clientId, discordSettings.enabled]); + }, [discordSettings.clientId, privateMode, discordSettings.enabled]); useEffect(() => { - if (!discordSettings.enabled) return; + if (!discordSettings.enabled || privateMode) return; const unsubSongChange = usePlayerStore.subscribe( (state) => [state.current.song, state.current.time, state.current.status], setActivity, @@ -155,5 +157,5 @@ export const useDiscordRpc = () => { return () => { unsubSongChange(); }; - }, [discordSettings.enabled, setActivity]); + }, [discordSettings.enabled, privateMode, setActivity]); }; diff --git a/src/renderer/features/player/hooks/use-scrobble.ts b/src/renderer/features/player/hooks/use-scrobble.ts index 3df9ab01..bb52cbde 100644 --- a/src/renderer/features/player/hooks/use-scrobble.ts +++ b/src/renderer/features/player/hooks/use-scrobble.ts @@ -1,8 +1,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useSendScrobble } from '/@/renderer/features/player/mutations/scrobble-mutation'; -import { usePlayerStore } from '/@/renderer/store'; -import { usePlaybackSettings } from '/@/renderer/store/settings.store'; +import { useAppStore, usePlaybackSettings, usePlayerStore } from '/@/renderer/store'; import { QueueSong, ServerType } from '/@/shared/types/domain-types'; import { PlayerStatus } from '/@/shared/types/types'; @@ -59,13 +58,14 @@ const checkScrobbleConditions = (args: { export const useScrobble = () => { const scrobbleSettings = usePlaybackSettings().scrobble; const isScrobbleEnabled = scrobbleSettings?.enabled; + const isPrivateModeEnabled = useAppStore().privateMode; const sendScrobble = useSendScrobble(); const [isCurrentSongScrobbled, setIsCurrentSongScrobbled] = useState(false); const handleScrobbleFromSeek = useCallback( (currentTime: number) => { - if (!isScrobbleEnabled) return; + if (!isScrobbleEnabled || isPrivateModeEnabled) return; const currentSong = usePlayerStore.getState().current.song; @@ -84,7 +84,7 @@ export const useScrobble = () => { serverId: currentSong?.serverId, }); }, - [isScrobbleEnabled, sendScrobble], + [isScrobbleEnabled, isPrivateModeEnabled, sendScrobble], ); const progressIntervalId = useRef>(null); @@ -119,7 +119,7 @@ export const useScrobble = () => { }, 1000); } - if (!isScrobbleEnabled) return; + if (!isScrobbleEnabled || isPrivateModeEnabled) return; if (progressIntervalId.current) { clearInterval(progressIntervalId.current); @@ -201,6 +201,7 @@ export const useScrobble = () => { scrobbleSettings?.scrobbleAtDuration, scrobbleSettings?.scrobbleAtPercentage, isScrobbleEnabled, + isPrivateModeEnabled, isCurrentSongScrobbled, sendScrobble, handleScrobbleFromSeek, @@ -209,7 +210,7 @@ export const useScrobble = () => { const handleScrobbleFromStatusChange = useCallback( (current: PlayerEvent, previous: PlayerEvent) => { - if (!isScrobbleEnabled) return; + if (!isScrobbleEnabled || isPrivateModeEnabled) return; const currentSong = usePlayerStore.getState().current.song; @@ -293,6 +294,7 @@ export const useScrobble = () => { }, [ isScrobbleEnabled, + isPrivateModeEnabled, sendScrobble, handleScrobbleFromSeek, scrobbleSettings?.scrobbleAtDuration, @@ -306,7 +308,7 @@ export const useScrobble = () => { // need to perform another check to see if the scrobble conditions are met const handleScrobbleFromSongRestart = useCallback( (currentTime: number) => { - if (!isScrobbleEnabled) return; + if (!isScrobbleEnabled || isPrivateModeEnabled) return; const currentSong = usePlayerStore.getState().current.song; @@ -349,6 +351,7 @@ export const useScrobble = () => { }, [ isScrobbleEnabled, + isPrivateModeEnabled, scrobbleSettings?.scrobbleAtDuration, scrobbleSettings?.scrobbleAtPercentage, isCurrentSongScrobbled, diff --git a/src/renderer/features/titlebar/components/app-menu.tsx b/src/renderer/features/titlebar/components/app-menu.tsx index 1d0733ae..a240d22d 100644 --- a/src/renderer/features/titlebar/components/app-menu.tsx +++ b/src/renderer/features/titlebar/components/app-menu.tsx @@ -10,6 +10,7 @@ import { ServerList } from '/@/renderer/features/servers'; import { EditServerForm } from '/@/renderer/features/servers/components/edit-server-form'; import { AppRoute } from '/@/renderer/router/routes'; import { + useAppStore, useAppStoreActions, useAuthStoreActions, useCurrentServer, @@ -30,7 +31,8 @@ export const AppMenu = () => { const serverList = useServerList(); const { setCurrentServer } = useAuthStoreActions(); const { collapsed } = useSidebarStore(); - const { setSideBar } = useAppStoreActions(); + const { privateMode } = useAppStore(); + const { setPrivateMode, setSideBar } = useAppStoreActions(); const handleSetCurrentServer = (server: ServerListItem) => { navigate(AppRoute.HOME); @@ -80,6 +82,14 @@ export const AppMenu = () => { setSideBar({ collapsed: false }); }; + const handlePrivateModeOff = () => { + setPrivateMode(false); + }; + + const handlePrivateModeOn = () => { + setPrivateMode(true); + }; + const handleQuit = () => { browser?.quit(); }; @@ -127,7 +137,18 @@ export const AppMenu = () => { > {t('page.appMenu.manageServers', { postProcess: 'sentenceCase' })} - + {privateMode ? ( + } + onClick={handlePrivateModeOff} + > + {t('page.appMenu.privateModeOff', { postProcess: 'sentenceCase' })} + + ) : ( + } onClick={handlePrivateModeOn}> + {t('page.appMenu.privateModeOn', { postProcess: 'sentenceCase' })} + + )} {t('page.appMenu.selectServer', { postProcess: 'sentenceCase' })} diff --git a/src/renderer/store/app.store.ts b/src/renderer/store/app.store.ts index 83074f5d..68f5bfd3 100644 --- a/src/renderer/store/app.store.ts +++ b/src/renderer/store/app.store.ts @@ -8,6 +8,7 @@ import { Platform } from '/@/shared/types/types'; export interface AppSlice extends AppState { actions: { setAppStore: (data: Partial) => void; + setPrivateMode: (enabled: boolean) => void; setSideBar: (options: Partial) => void; setTitleBar: (options: Partial) => void; }; @@ -17,6 +18,7 @@ export interface AppState { commandPalette: CommandPaletteProps; isReorderingQueue: boolean; platform: Platform; + privateMode: boolean; sidebar: SidebarProps; titlebar: TitlebarProps; } @@ -50,6 +52,11 @@ export const useAppStore = createWithEqualityFn()( setAppStore: (data) => { set({ ...get(), ...data }); }, + setPrivateMode: (privateMode) => { + set((state) => { + state.privateMode = privateMode; + }); + }, setSideBar: (options) => { set((state) => { state.sidebar = { ...state.sidebar, ...options }; @@ -81,6 +88,7 @@ export const useAppStore = createWithEqualityFn()( }, isReorderingQueue: false, platform: Platform.WINDOWS, + privateMode: false, sidebar: { collapsed: false, expanded: [], diff --git a/src/shared/components/icon/icon.tsx b/src/shared/components/icon/icon.tsx index d77b5552..fc4e3174 100644 --- a/src/shared/components/icon/icon.tsx +++ b/src/shared/components/icon/icon.tsx @@ -59,6 +59,7 @@ import { LuListPlus, LuLoader, LuLock, + LuLockOpen, LuLogIn, LuLogOut, LuMenu, @@ -163,6 +164,7 @@ export const AppIcon = { listInfinite: LuInfinity, listPaginated: LuArrowRightToLine, lock: LuLock, + lockOpen: LuLockOpen, mediaNext: LuSkipForward, mediaPause: LuPause, mediaPlay: LuPlay, From 7423b71a6598d699347af09a0edac8a9a237ee27 Mon Sep 17 00:00:00 2001 From: Lyall Date: Thu, 31 Jul 2025 16:24:41 +0100 Subject: [PATCH 2/3] change text --- src/i18n/locales/en.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 414f277a..ab3a9aad 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -321,8 +321,8 @@ "goBack": "go back", "goForward": "go forward", "manageServers": "manage servers", - "privateModeOff": "turn private mode off", - "privateModeOn": "turn private mode on", + "privateModeOff": "turn off private mode", + "privateModeOn": "turn on private mode", "openBrowserDevtools": "open browser devtools", "quit": "$t(common.quit)", "selectServer": "select server", From 2271c211cb10f020f48030047ff4bad8597bd7f6 Mon Sep 17 00:00:00 2001 From: Lyall Date: Thu, 28 Aug 2025 05:48:21 +0100 Subject: [PATCH 3/3] display private mode status --- src/i18n/locales/en.json | 5 +++++ .../features/titlebar/components/app-menu.tsx | 16 +++++++++++++-- src/renderer/layouts/window-bar.tsx | 20 ++++++++++++------- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index ab3a9aad..5e64bdb3 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -288,6 +288,11 @@ "updateServer": { "success": "server updated successfully", "title": "update server" + }, + "privateMode": { + "enabled": "private mode enabled, playback status is now hidden from external integrations", + "disabled": "private mode disabled, playback status is now visible to enabled external integrations", + "title": "private mode" } }, "page": { diff --git a/src/renderer/features/titlebar/components/app-menu.tsx b/src/renderer/features/titlebar/components/app-menu.tsx index a240d22d..9c56a35e 100644 --- a/src/renderer/features/titlebar/components/app-menu.tsx +++ b/src/renderer/features/titlebar/components/app-menu.tsx @@ -19,6 +19,7 @@ import { } from '/@/renderer/store'; import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu'; import { Icon } from '/@/shared/components/icon/icon'; +import { toast } from '/@/shared/components/toast/toast'; import { ServerListItem, ServerType } from '/@/shared/types/domain-types'; const browser = isElectron() ? window.api.browser : null; @@ -84,10 +85,18 @@ export const AppMenu = () => { const handlePrivateModeOff = () => { setPrivateMode(false); + toast.info({ + message: t('form.privateMode.disabled', { postProcess: 'sentenceCase' }), + title: t('form.privateMode.title', { postProcess: 'sentenceCase' }), + }); }; const handlePrivateModeOn = () => { setPrivateMode(true); + toast.info({ + message: t('form.privateMode.enabled', { postProcess: 'sentenceCase' }), + title: t('form.privateMode.title', { postProcess: 'sentenceCase' }), + }); }; const handleQuit = () => { @@ -139,13 +148,16 @@ export const AppMenu = () => { {privateMode ? ( } + leftSection={} onClick={handlePrivateModeOff} > {t('page.appMenu.privateModeOff', { postProcess: 'sentenceCase' })} ) : ( - } onClick={handlePrivateModeOn}> + } + onClick={handlePrivateModeOn} + > {t('page.appMenu.privateModeOn', { postProcess: 'sentenceCase' })} )} diff --git a/src/renderer/layouts/window-bar.tsx b/src/renderer/layouts/window-bar.tsx index 3164405a..3d65bd1d 100644 --- a/src/renderer/layouts/window-bar.tsx +++ b/src/renderer/layouts/window-bar.tsx @@ -12,8 +12,12 @@ import macMinHover from './assets/min-mac-hover.png'; import macMin from './assets/min-mac.png'; import styles from './window-bar.module.css'; -import { useCurrentStatus, useQueueStatus } from '/@/renderer/store'; -import { useWindowSettings } from '/@/renderer/store/settings.store'; +import { + useAppStore, + useCurrentStatus, + useQueueStatus, + useWindowSettings, +} from '/@/renderer/store'; import { Text } from '/@/shared/components/text/text'; import { Platform, PlayerStatus } from '/@/shared/types/types'; @@ -126,14 +130,16 @@ export const WindowBar = () => { const playerStatus = useCurrentStatus(); const { currentSong, index, length } = useQueueStatus(); const { windowBarStyle } = useWindowSettings(); + const { privateMode } = useAppStore(); const statusString = playerStatus === PlayerStatus.PAUSED ? '(Paused) ' : ''; const queueString = length ? `(${index + 1} / ${length}) ` : ''; - const title = length - ? currentSong?.artistName - ? `${statusString}${queueString}${currentSong?.name} — ${currentSong?.artistName}` - : `${statusString}${queueString}${currentSong?.name}` - : 'Feishin'; + const privateModeString = privateMode ? '(Private mode)' : ''; + const title = `${ + length + ? `${statusString}${queueString}${currentSong?.name}${currentSong?.artistName ? ` — ${currentSong?.artistName}` : ''}` + : 'Feishin' + }${privateMode ? ` ${privateModeString}` : ''}`; document.title = title; const [max, setMax] = useState(localSettings?.env.START_MAXIMIZED || false);