From a6cbaf6dc11b8cd031b8129eb729c91b43e12192 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 7 Nov 2025 17:48:56 +0200 Subject: [PATCH 1/9] feat: custom achievement sound and volume changing) --- src/locales/en/translation.json | 5 + src/main/constants.ts | 4 + src/main/events/index.ts | 4 + .../themes/copy-theme-achievement-sound.ts | 40 ++++++ .../events/themes/get-theme-sound-path.ts | 12 ++ .../themes/import-theme-sound-from-store.ts | 57 +++++++++ .../themes/remove-theme-achievement-sound.ts | 39 ++++++ src/main/helpers/index.ts | 24 ++++ src/main/services/notifications/index.ts | 45 ++++++- src/preload/index.ts | 8 ++ src/renderer/src/app.tsx | 11 +- src/renderer/src/declaration.d.ts | 11 ++ src/renderer/src/helpers.ts | 29 +++++ .../notification/achievement-notification.tsx | 11 +- .../aparence/modals/import-theme-modal.tsx | 10 ++ .../src/pages/settings/settings-general.scss | 96 ++++++++++++++ .../src/pages/settings/settings-general.tsx | 83 +++++++++++- .../src/pages/theme-editor/theme-editor.scss | 17 ++- .../src/pages/theme-editor/theme-editor.tsx | 121 +++++++++++++----- src/types/level.types.ts | 1 + src/types/theme.types.ts | 1 + 21 files changed, 583 insertions(+), 46 deletions(-) create mode 100644 src/main/events/themes/copy-theme-achievement-sound.ts create mode 100644 src/main/events/themes/get-theme-sound-path.ts create mode 100644 src/main/events/themes/import-theme-sound-from-store.ts create mode 100644 src/main/events/themes/remove-theme-achievement-sound.ts diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 9989f153..30b165ae 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -557,6 +557,11 @@ "platinum": "Platinum", "hidden": "Hidden", "test_notification": "Test notification", + "achievement_sound_volume": "Achievement sound volume", + "select_achievement_sound": "Select achievement sound", + "change_achievement_sound": "Change achievement sound", + "remove_achievement_sound": "Remove achievement sound", + "preview_sound": "Preview sound", "notification_preview": "Achievement Notification Preview", "enable_friend_start_game_notifications": "When a friend starts playing a game", "autoplay_trailers_on_game_page": "Automatically start playing trailers on game page", diff --git a/src/main/constants.ts b/src/main/constants.ts index 82b99b2a..3c4c10e5 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -41,8 +41,12 @@ export const appVersion = app.getVersion() + (isStaging ? "-staging" : ""); export const ASSETS_PATH = path.join(SystemPath.getPath("userData"), "Assets"); +export const THEMES_PATH = path.join(SystemPath.getPath("userData"), "themes"); + export const MAIN_LOOP_INTERVAL = 2000; +export const DEFAULT_ACHIEVEMENT_SOUND_VOLUME = 0.15; + export const DECKY_PLUGINS_LOCATION = path.join( SystemPath.getPath("home"), "homebrew", diff --git a/src/main/events/index.ts b/src/main/events/index.ts index aaac89dd..89dd01f5 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -92,6 +92,10 @@ import "./themes/get-custom-theme-by-id"; import "./themes/get-active-custom-theme"; import "./themes/close-editor-window"; import "./themes/toggle-custom-theme"; +import "./themes/copy-theme-achievement-sound"; +import "./themes/remove-theme-achievement-sound"; +import "./themes/get-theme-sound-path"; +import "./themes/import-theme-sound-from-store"; import "./download-sources/remove-download-source"; import "./download-sources/get-download-sources"; import { isPortableVersion } from "@main/helpers"; diff --git a/src/main/events/themes/copy-theme-achievement-sound.ts b/src/main/events/themes/copy-theme-achievement-sound.ts new file mode 100644 index 00000000..72ec0c79 --- /dev/null +++ b/src/main/events/themes/copy-theme-achievement-sound.ts @@ -0,0 +1,40 @@ +import { registerEvent } from "../register-event"; +import fs from "node:fs"; +import path from "node:path"; +import { getThemePath } from "@main/helpers"; +import { themesSublevel } from "@main/level"; + +const copyThemeAchievementSound = async ( + _event: Electron.IpcMainInvokeEvent, + themeId: string, + sourcePath: string +): Promise => { + if (!sourcePath || !fs.existsSync(sourcePath)) { + throw new Error("Source file does not exist"); + } + + const theme = await themesSublevel.get(themeId); + if (!theme) { + throw new Error("Theme not found"); + } + + const themeDir = getThemePath(themeId); + + if (!fs.existsSync(themeDir)) { + fs.mkdirSync(themeDir, { recursive: true }); + } + + const fileExtension = path.extname(sourcePath); + const destinationPath = path.join(themeDir, `achievement${fileExtension}`); + + await fs.promises.copyFile(sourcePath, destinationPath); + + await themesSublevel.put(themeId, { + ...theme, + hasCustomSound: true, + updatedAt: new Date(), + }); +}; + +registerEvent("copyThemeAchievementSound", copyThemeAchievementSound); + diff --git a/src/main/events/themes/get-theme-sound-path.ts b/src/main/events/themes/get-theme-sound-path.ts new file mode 100644 index 00000000..5dccbd4e --- /dev/null +++ b/src/main/events/themes/get-theme-sound-path.ts @@ -0,0 +1,12 @@ +import { registerEvent } from "../register-event"; +import { getThemeSoundPath } from "@main/helpers"; + +const getThemeSoundPathEvent = async ( + _event: Electron.IpcMainInvokeEvent, + themeId: string +): Promise => { + return getThemeSoundPath(themeId); +}; + +registerEvent("getThemeSoundPath", getThemeSoundPathEvent); + diff --git a/src/main/events/themes/import-theme-sound-from-store.ts b/src/main/events/themes/import-theme-sound-from-store.ts new file mode 100644 index 00000000..cd4c6fcd --- /dev/null +++ b/src/main/events/themes/import-theme-sound-from-store.ts @@ -0,0 +1,57 @@ +import { registerEvent } from "../register-event"; +import fs from "node:fs"; +import path from "node:path"; +import axios from "axios"; +import { getThemePath } from "@main/helpers"; +import { themesSublevel } from "@main/level"; +import { logger } from "@main/services"; + +const importThemeSoundFromStore = async ( + _event: Electron.IpcMainInvokeEvent, + themeId: string, + themeName: string, + storeUrl: string +): Promise => { + const theme = await themesSublevel.get(themeId); + if (!theme) { + throw new Error("Theme not found"); + } + + const formats = ["wav", "mp3", "ogg", "m4a"]; + + for (const format of formats) { + try { + const soundUrl = `${storeUrl}/themes/${themeName.toLowerCase()}/achievement.${format}`; + + const response = await axios.get(soundUrl, { + responseType: "arraybuffer", + timeout: 10000, + }); + + const themeDir = getThemePath(themeId); + + if (!fs.existsSync(themeDir)) { + fs.mkdirSync(themeDir, { recursive: true }); + } + + const destinationPath = path.join(themeDir, `achievement.${format}`); + await fs.promises.writeFile(destinationPath, response.data); + + await themesSublevel.put(themeId, { + ...theme, + hasCustomSound: true, + updatedAt: new Date(), + }); + + logger.log(`Successfully imported sound for theme ${themeName}`); + return; + } catch (error) { + continue; + } + } + + logger.log(`No sound file found for theme ${themeName} in store`); +}; + +registerEvent("importThemeSoundFromStore", importThemeSoundFromStore); + diff --git a/src/main/events/themes/remove-theme-achievement-sound.ts b/src/main/events/themes/remove-theme-achievement-sound.ts new file mode 100644 index 00000000..adb17a57 --- /dev/null +++ b/src/main/events/themes/remove-theme-achievement-sound.ts @@ -0,0 +1,39 @@ +import { registerEvent } from "../register-event"; +import fs from "node:fs"; +import { getThemePath } from "@main/helpers"; +import { themesSublevel } from "@main/level"; +import path from "node:path"; + +const removeThemeAchievementSound = async ( + _event: Electron.IpcMainInvokeEvent, + themeId: string +): Promise => { + const theme = await themesSublevel.get(themeId); + if (!theme) { + throw new Error("Theme not found"); + } + + const themeDir = getThemePath(themeId); + + if (!fs.existsSync(themeDir)) { + return; + } + + const formats = ["wav", "mp3", "ogg", "m4a"]; + + for (const format of formats) { + const soundPath = path.join(themeDir, `achievement.${format}`); + if (fs.existsSync(soundPath)) { + await fs.promises.unlink(soundPath); + } + } + + await themesSublevel.put(themeId, { + ...theme, + hasCustomSound: false, + updatedAt: new Date(), + }); +}; + +registerEvent("removeThemeAchievementSound", removeThemeAchievementSound); + diff --git a/src/main/helpers/index.ts b/src/main/helpers/index.ts index 2da49a1c..ae19fbdb 100644 --- a/src/main/helpers/index.ts +++ b/src/main/helpers/index.ts @@ -2,6 +2,8 @@ import axios from "axios"; import { JSDOM } from "jsdom"; import UserAgent from "user-agents"; import path from "node:path"; +import fs from "node:fs"; +import { THEMES_PATH } from "@main/constants"; export const getFileBuffer = async (url: string) => fetch(url, { method: "GET" }).then((response) => @@ -36,4 +38,26 @@ export const normalizePath = (str: string) => export const addTrailingSlash = (str: string) => str.endsWith("/") ? str : `${str}/`; +export const getThemePath = (themeId: string) => + path.join(THEMES_PATH, themeId); + +export const getThemeSoundPath = (themeId: string): string | null => { + const themeDir = getThemePath(themeId); + + if (!fs.existsSync(themeDir)) { + return null; + } + + const formats = ["wav", "mp3", "ogg", "m4a"]; + + for (const format of formats) { + const soundPath = path.join(themeDir, `achievement.${format}`); + if (fs.existsSync(soundPath)) { + return soundPath; + } + } + + return null; +}; + export * from "./reg-parser"; diff --git a/src/main/services/notifications/index.ts b/src/main/services/notifications/index.ts index d28c3cd7..d78a4d3f 100644 --- a/src/main/services/notifications/index.ts +++ b/src/main/services/notifications/index.ts @@ -5,15 +5,16 @@ import fs from "node:fs"; import axios from "axios"; import path from "node:path"; import sound from "sound-play"; -import { achievementSoundPath } from "@main/constants"; +import { achievementSoundPath, DEFAULT_ACHIEVEMENT_SOUND_VOLUME } from "@main/constants"; import icon from "@resources/icon.png?asset"; import { NotificationOptions, toXmlString } from "./xml"; import { logger } from "../logger"; import { WindowManager } from "../window-manager"; import type { Game, UserPreferences, UserProfile } from "@types"; -import { db, levelKeys } from "@main/level"; +import { db, levelKeys, themesSublevel } from "@main/level"; import { restartAndInstallUpdate } from "@main/events/autoupdater/restart-and-install-update"; import { SystemPath } from "../system-path"; +import { getThemeSoundPath } from "@main/helpers"; async function downloadImage(url: string | null) { if (!url) return undefined; @@ -40,6 +41,40 @@ async function downloadImage(url: string | null) { }); } +async function getAchievementSoundPath(): Promise { + try { + const allThemes = await themesSublevel.values().all(); + const activeTheme = allThemes.find((theme) => theme.isActive); + + if (activeTheme) { + const themeSoundPath = getThemeSoundPath(activeTheme.id); + if (themeSoundPath) { + return themeSoundPath; + } + } + } catch (error) { + logger.error("Failed to get theme sound path", error); + } + + return achievementSoundPath; +} + +async function getAchievementSoundVolume(): Promise { + try { + const userPreferences = await db.get( + levelKeys.userPreferences, + { + valueEncoding: "json", + } + ); + + return userPreferences?.achievementSoundVolume ?? DEFAULT_ACHIEVEMENT_SOUND_VOLUME; + } catch (error) { + logger.error("Failed to get achievement sound volume", error); + return DEFAULT_ACHIEVEMENT_SOUND_VOLUME; + } +} + export const publishDownloadCompleteNotification = async (game: Game) => { const userPreferences = await db.get( levelKeys.userPreferences, @@ -145,7 +180,8 @@ export const publishCombinedNewAchievementNotification = async ( if (WindowManager.mainWindow) { WindowManager.mainWindow.webContents.send("on-achievement-unlocked"); } else if (process.platform !== "linux") { - sound.play(achievementSoundPath); + const soundPath = await getAchievementSoundPath(); + sound.play(soundPath); } }; @@ -205,6 +241,7 @@ export const publishNewAchievementNotification = async (info: { if (WindowManager.mainWindow) { WindowManager.mainWindow.webContents.send("on-achievement-unlocked"); } else if (process.platform !== "linux") { - sound.play(achievementSoundPath); + const soundPath = await getAchievementSoundPath(); + sound.play(soundPath); } }; diff --git a/src/preload/index.ts b/src/preload/index.ts index fc588a30..67ac0c73 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -571,6 +571,14 @@ contextBridge.exposeInMainWorld("electron", { getActiveCustomTheme: () => ipcRenderer.invoke("getActiveCustomTheme"), toggleCustomTheme: (themeId: string, isActive: boolean) => ipcRenderer.invoke("toggleCustomTheme", themeId, isActive), + copyThemeAchievementSound: (themeId: string, sourcePath: string) => + ipcRenderer.invoke("copyThemeAchievementSound", themeId, sourcePath), + removeThemeAchievementSound: (themeId: string) => + ipcRenderer.invoke("removeThemeAchievementSound", themeId), + getThemeSoundPath: (themeId: string) => + ipcRenderer.invoke("getThemeSoundPath", themeId), + importThemeSoundFromStore: (themeId: string, themeName: string, storeUrl: string) => + ipcRenderer.invoke("importThemeSoundFromStore", themeId, themeName, storeUrl), /* Editor */ openEditorWindow: (themeId: string) => diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index 1ab76381..fd1c2735 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -1,5 +1,4 @@ import { useCallback, useEffect, useRef } from "react"; -import achievementSound from "@renderer/assets/audio/achievement.wav"; import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components"; import { @@ -25,7 +24,7 @@ import { UserFriendModal } from "./pages/shared-modals/user-friend-modal"; import { useSubscription } from "./hooks/use-subscription"; import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal"; -import { injectCustomCss, removeCustomCss } from "./helpers"; +import { injectCustomCss, removeCustomCss, getAchievementSoundUrl, getAchievementSoundVolume } from "./helpers"; import "./app.scss"; export interface AppProps { @@ -216,9 +215,11 @@ export function App() { return () => unsubscribe(); }, [loadAndApplyTheme]); - const playAudio = useCallback(() => { - const audio = new Audio(achievementSound); - audio.volume = 0.2; + const playAudio = useCallback(async () => { + const soundUrl = await getAchievementSoundUrl(); + const volume = await getAchievementSoundVolume(); + const audio = new Audio(soundUrl); + audio.volume = volume; audio.play(); }, []); diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 65f2ce9e..44696872 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -410,6 +410,17 @@ declare global { getCustomThemeById: (themeId: string) => Promise; getActiveCustomTheme: () => Promise; toggleCustomTheme: (themeId: string, isActive: boolean) => Promise; + copyThemeAchievementSound: ( + themeId: string, + sourcePath: string + ) => Promise; + removeThemeAchievementSound: (themeId: string) => Promise; + getThemeSoundPath: (themeId: string) => Promise; + importThemeSoundFromStore: ( + themeId: string, + themeName: string, + storeUrl: string + ) => Promise; /* Editor */ openEditorWindow: (themeId: string) => Promise; diff --git a/src/renderer/src/helpers.ts b/src/renderer/src/helpers.ts index f09cec84..3ee04805 100644 --- a/src/renderer/src/helpers.ts +++ b/src/renderer/src/helpers.ts @@ -121,3 +121,32 @@ export const formatNumber = (num: number): string => { export const generateUUID = (): string => { return uuidv4(); }; + +export const getAchievementSoundUrl = async (): Promise => { + const defaultSound = (await import("@renderer/assets/audio/achievement.wav")).default; + + try { + const activeTheme = await window.electron.getActiveCustomTheme(); + + if (activeTheme?.hasCustomSound) { + const soundPath = await window.electron.getThemeSoundPath(activeTheme.id); + if (soundPath) { + return `file://${soundPath}`; + } + } + } catch (error) { + console.error("Failed to get theme sound", error); + } + + return defaultSound; +}; + +export const getAchievementSoundVolume = async (): Promise => { + try { + const prefs = await window.electron.getUserPreferences(); + return prefs?.achievementSoundVolume ?? 0.15; + } catch (error) { + console.error("Failed to get sound volume", error); + return 0.15; + } +}; diff --git a/src/renderer/src/pages/achievements/notification/achievement-notification.tsx b/src/renderer/src/pages/achievements/notification/achievement-notification.tsx index c5c37933..02023f1d 100644 --- a/src/renderer/src/pages/achievements/notification/achievement-notification.tsx +++ b/src/renderer/src/pages/achievements/notification/achievement-notification.tsx @@ -1,11 +1,10 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import achievementSound from "@renderer/assets/audio/achievement.wav"; import { useTranslation } from "react-i18next"; import { AchievementCustomNotificationPosition, AchievementNotificationInfo, } from "@types"; -import { injectCustomCss, removeCustomCss } from "@renderer/helpers"; +import { injectCustomCss, removeCustomCss, getAchievementSoundUrl, getAchievementSoundVolume } from "@renderer/helpers"; import { AchievementNotificationItem } from "@renderer/components/achievements/notification/achievement-notification"; import app from "../../../app.scss?inline"; import styles from "../../../components/achievements/notification/achievement-notification.scss?inline"; @@ -33,9 +32,11 @@ export function AchievementNotification() { const [shadowRootRef, setShadowRootRef] = useState(null); - const playAudio = useCallback(() => { - const audio = new Audio(achievementSound); - audio.volume = 0.1; + const playAudio = useCallback(async () => { + const soundUrl = await getAchievementSoundUrl(); + const volume = await getAchievementSoundVolume(); + const audio = new Audio(soundUrl); + audio.volume = volume; audio.play(); }, []); diff --git a/src/renderer/src/pages/settings/aparence/modals/import-theme-modal.tsx b/src/renderer/src/pages/settings/aparence/modals/import-theme-modal.tsx index 516f320f..93baf1cd 100644 --- a/src/renderer/src/pages/settings/aparence/modals/import-theme-modal.tsx +++ b/src/renderer/src/pages/settings/aparence/modals/import-theme-modal.tsx @@ -51,6 +51,16 @@ export const ImportThemeModal = ({ if (!currentTheme) return; + try { + await window.electron.importThemeSoundFromStore( + theme.id, + themeName, + THEME_WEB_STORE_URL + ); + } catch (soundError) { + logger.error("Failed to import theme sound", soundError); + } + const activeTheme = await window.electron.getActiveCustomTheme(); if (activeTheme) { diff --git a/src/renderer/src/pages/settings/settings-general.scss b/src/renderer/src/pages/settings/settings-general.scss index 302effa3..58004362 100644 --- a/src/renderer/src/pages/settings/settings-general.scss +++ b/src/renderer/src/pages/settings/settings-general.scss @@ -17,4 +17,100 @@ &__test-achievement-notification-button { align-self: flex-start; } + + &__volume-control { + display: flex; + flex-direction: column; + gap: 8px; + + label { + font-size: 14px; + color: globals.$muted-color; + } + } + + &__volume-input-wrapper { + display: flex; + align-items: center; + gap: 4px; + } + + &__volume-input-container { + position: relative; + display: flex; + align-items: center; + background: globals.$dark-background-color; + border: 1px solid globals.$border-color; + padding: 8px 8px; + border-radius: 4px; + transition: border-color 0.2s; + + &:focus-within { + border-color: rgba(255, 255, 255, 0.5); + color: globals.$muted-color; + } + + input[type="number"] { + width: 30px; + background: transparent; + border: none; + color: globals.$muted-color; + font-size: 14px; + text-align: center; + + &:focus { + outline: none; + } + + &::-webkit-inner-spin-button, + &::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } + + appearance: textfield; + -moz-appearance: textfield; + } + } + + &__volume-input-unit { + font-size: 14px; + color: globals.$muted-color; + pointer-events: none; + user-select: none; + } + + &__volume-input-buttons { + display: flex; + flex-direction: column; + gap: 2px; + + button { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 16px; + background: globals.$dark-background-color; + border: 1px solid globals.$border-color; + border-radius: 2px; + color: globals.$muted-color; + cursor: pointer; + transition: all 0.2s; + + &:hover { + color: globals.$muted-color; + border-color: rgba(255, 255, 255, 0.5); + } + + &:active { + background: globals.$background-color; + } + + svg { + width: 12px; + height: 12px; + } + } + } } diff --git a/src/renderer/src/pages/settings/settings-general.tsx b/src/renderer/src/pages/settings/settings-general.tsx index c698440d..6d81f763 100644 --- a/src/renderer/src/pages/settings/settings-general.tsx +++ b/src/renderer/src/pages/settings/settings-general.tsx @@ -1,4 +1,4 @@ -import { useContext, useEffect, useMemo, useState } from "react"; +import { useContext, useEffect, useMemo, useState, useCallback, useRef } from "react"; import { TextField, Button, @@ -43,6 +43,7 @@ export function SettingsGeneral() { achievementCustomNotificationsEnabled: true, achievementCustomNotificationPosition: "top-left" as AchievementCustomNotificationPosition, + achievementSoundVolume: 15, language: "", customStyles: window.localStorage.getItem("customStyles") || "", }); @@ -50,6 +51,8 @@ export function SettingsGeneral() { const [languageOptions, setLanguageOptions] = useState([]); const [defaultDownloadsPath, setDefaultDownloadsPath] = useState(""); + + const volumeUpdateTimeoutRef = useRef(); useEffect(() => { window.electron.getDefaultDownloadsPath().then((path) => { @@ -81,6 +84,9 @@ export function SettingsGeneral() { return () => { clearInterval(interval); + if (volumeUpdateTimeoutRef.current) { + clearTimeout(volumeUpdateTimeoutRef.current); + } }; }, []); @@ -110,6 +116,7 @@ export function SettingsGeneral() { userPreferences.achievementCustomNotificationsEnabled ?? true, achievementCustomNotificationPosition: userPreferences.achievementCustomNotificationPosition ?? "top-left", + achievementSoundVolume: Math.round((userPreferences.achievementSoundVolume ?? 0.15) * 100), friendRequestNotificationsEnabled: userPreferences.friendRequestNotificationsEnabled ?? false, friendStartGameNotificationsEnabled: @@ -148,6 +155,18 @@ export function SettingsGeneral() { await updateUserPreferences(values); }; + const handleVolumeChange = useCallback((newVolume: number) => { + setForm((prev) => ({ ...prev, achievementSoundVolume: newVolume })); + + if (volumeUpdateTimeoutRef.current) { + clearTimeout(volumeUpdateTimeoutRef.current); + } + + volumeUpdateTimeoutRef.current = setTimeout(() => { + updateUserPreferences({ achievementSoundVolume: newVolume / 100 }); + }, 300); + }, [updateUserPreferences]); + const handleChangeAchievementCustomNotificationPosition = async ( event: React.ChangeEvent ) => { @@ -309,6 +328,68 @@ export function SettingsGeneral() { )} + {form.achievementNotificationsEnabled && ( +
+ +
+
+ { + const value = e.target.value; + if (value === "") { + handleVolumeChange(0); + return; + } + const volumePercent = Math.min(100, Math.max(0, parseInt(value, 10))); + if (!isNaN(volumePercent)) { + handleVolumeChange(volumePercent); + } + }} + onBlur={(e) => { + if (e.target.value === "" || isNaN(parseInt(e.target.value, 10))) { + handleVolumeChange(0); + } + }} + /> + % +
+
+ + +
+
+
+ )} +

{t("common_redist")}

diff --git a/src/renderer/src/pages/theme-editor/theme-editor.scss b/src/renderer/src/pages/theme-editor/theme-editor.scss index 38061c88..b34217f9 100644 --- a/src/renderer/src/pages/theme-editor/theme-editor.scss +++ b/src/renderer/src/pages/theme-editor/theme-editor.scss @@ -93,12 +93,25 @@ &__notification-preview { padding-top: 12px; display: flex; - flex-direction: row; - align-items: center; + flex-direction: column; gap: 16px; &__select-variation { flex: inherit; } } + + &__notification-controls { + display: flex; + flex-direction: row; + align-items: center; + gap: 16px; + } + + &__sound-controls { + display: flex; + flex-direction: row; + gap: 8px; + flex-wrap: wrap; + } } diff --git a/src/renderer/src/pages/theme-editor/theme-editor.tsx b/src/renderer/src/pages/theme-editor/theme-editor.tsx index 9df3e9f4..8ae86f63 100644 --- a/src/renderer/src/pages/theme-editor/theme-editor.tsx +++ b/src/renderer/src/pages/theme-editor/theme-editor.tsx @@ -4,10 +4,10 @@ import Editor from "@monaco-editor/react"; import { AchievementCustomNotificationPosition, Theme } from "@types"; import { useSearchParams } from "react-router-dom"; import { Button, SelectField } from "@renderer/components"; -import { CheckIcon } from "@primer/octicons-react"; +import { CheckIcon, UploadIcon, TrashIcon, PlayIcon } from "@primer/octicons-react"; import { useTranslation } from "react-i18next"; import cn from "classnames"; -import { injectCustomCss } from "@renderer/helpers"; +import { injectCustomCss, getAchievementSoundUrl, getAchievementSoundVolume } from "@renderer/helpers"; import { AchievementNotificationItem } from "@renderer/components/achievements/notification/achievement-notification"; import { generateAchievementCustomNotificationTest } from "@shared"; import { CollapsedMenu } from "@renderer/components/collapsed-menu/collapsed-menu"; @@ -107,6 +107,46 @@ export default function ThemeEditor() { } }; + const handleSelectSound = useCallback(async () => { + if (!theme) return; + + const { filePaths } = await window.electron.showOpenDialog({ + properties: ["openFile"], + filters: [ + { + name: "Audio", + extensions: ["wav", "mp3", "ogg", "m4a"], + }, + ], + }); + + if (filePaths && filePaths.length > 0) { + await window.electron.copyThemeAchievementSound(theme.id, filePaths[0]); + const updatedTheme = await window.electron.getCustomThemeById(theme.id); + if (updatedTheme) { + setTheme(updatedTheme); + } + } + }, [theme]); + + const handleRemoveSound = useCallback(async () => { + if (!theme) return; + + await window.electron.removeThemeAchievementSound(theme.id); + const updatedTheme = await window.electron.getCustomThemeById(theme.id); + if (updatedTheme) { + setTheme(updatedTheme); + } + }, [theme]); + + const handlePreviewSound = useCallback(async () => { + const soundUrl = await getAchievementSoundUrl(); + const volume = await getAchievementSoundVolume(); + const audio = new Audio(soundUrl); + audio.volume = volume; + audio.play(); + }, []); + const achievementCustomNotificationPositionOptions = useMemo(() => { return [ "top-left", @@ -164,35 +204,58 @@ export default function ThemeEditor() {

- { - return { - key: variation, - value: variation, - label: t(variation), - }; +
+ { + return { + key: variation, + value: variation, + label: t(variation), + }; + } + )} + onChange={(value) => + setNotificationVariation( + value.target.value as keyof typeof notificationVariations + ) } - )} - onChange={(value) => - setNotificationVariation( - value.target.value as keyof typeof notificationVariations - ) - } - /> + /> - - setNotificationAlignment( - e.target.value as AchievementCustomNotificationPosition - ) - } - options={achievementCustomNotificationPositionOptions} - /> + + setNotificationAlignment( + e.target.value as AchievementCustomNotificationPosition + ) + } + options={achievementCustomNotificationPositionOptions} + /> +
+ +
+ + + {theme?.hasCustomSound && ( + + )} + + +
diff --git a/src/types/level.types.ts b/src/types/level.types.ts index 1df55b9e..c3f799fa 100644 --- a/src/types/level.types.ts +++ b/src/types/level.types.ts @@ -113,6 +113,7 @@ export interface UserPreferences { achievementNotificationsEnabled?: boolean; achievementCustomNotificationsEnabled?: boolean; achievementCustomNotificationPosition?: AchievementCustomNotificationPosition; + achievementSoundVolume?: number; friendRequestNotificationsEnabled?: boolean; friendStartGameNotificationsEnabled?: boolean; showDownloadSpeedInMegabytes?: boolean; diff --git a/src/types/theme.types.ts b/src/types/theme.types.ts index abba8fc1..94285d9c 100644 --- a/src/types/theme.types.ts +++ b/src/types/theme.types.ts @@ -5,6 +5,7 @@ export interface Theme { authorName?: string; isActive: boolean; code: string; + hasCustomSound?: boolean; createdAt: Date; updatedAt: Date; } From 154b6271a1379acf23d4799598ab872fd9565663 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 7 Nov 2025 17:50:47 +0200 Subject: [PATCH 2/9] fix: removed unused function --- .../themes/copy-theme-achievement-sound.ts | 1 - .../events/themes/get-theme-sound-path.ts | 1 - .../themes/import-theme-sound-from-store.ts | 3 +- .../themes/remove-theme-achievement-sound.ts | 1 - src/main/services/notifications/index.ts | 18 +---- src/preload/index.ts | 13 +++- src/renderer/src/app.tsx | 7 +- src/renderer/src/helpers.ts | 9 ++- .../notification/achievement-notification.tsx | 7 +- .../src/pages/settings/settings-general.tsx | 74 ++++++++++++++----- .../src/pages/theme-editor/theme-editor.tsx | 13 +++- 11 files changed, 95 insertions(+), 52 deletions(-) diff --git a/src/main/events/themes/copy-theme-achievement-sound.ts b/src/main/events/themes/copy-theme-achievement-sound.ts index 72ec0c79..aec22cb2 100644 --- a/src/main/events/themes/copy-theme-achievement-sound.ts +++ b/src/main/events/themes/copy-theme-achievement-sound.ts @@ -37,4 +37,3 @@ const copyThemeAchievementSound = async ( }; registerEvent("copyThemeAchievementSound", copyThemeAchievementSound); - diff --git a/src/main/events/themes/get-theme-sound-path.ts b/src/main/events/themes/get-theme-sound-path.ts index 5dccbd4e..37783949 100644 --- a/src/main/events/themes/get-theme-sound-path.ts +++ b/src/main/events/themes/get-theme-sound-path.ts @@ -9,4 +9,3 @@ const getThemeSoundPathEvent = async ( }; registerEvent("getThemeSoundPath", getThemeSoundPathEvent); - diff --git a/src/main/events/themes/import-theme-sound-from-store.ts b/src/main/events/themes/import-theme-sound-from-store.ts index cd4c6fcd..588db6f5 100644 --- a/src/main/events/themes/import-theme-sound-from-store.ts +++ b/src/main/events/themes/import-theme-sound-from-store.ts @@ -22,7 +22,7 @@ const importThemeSoundFromStore = async ( for (const format of formats) { try { const soundUrl = `${storeUrl}/themes/${themeName.toLowerCase()}/achievement.${format}`; - + const response = await axios.get(soundUrl, { responseType: "arraybuffer", timeout: 10000, @@ -54,4 +54,3 @@ const importThemeSoundFromStore = async ( }; registerEvent("importThemeSoundFromStore", importThemeSoundFromStore); - diff --git a/src/main/events/themes/remove-theme-achievement-sound.ts b/src/main/events/themes/remove-theme-achievement-sound.ts index adb17a57..4ca738f3 100644 --- a/src/main/events/themes/remove-theme-achievement-sound.ts +++ b/src/main/events/themes/remove-theme-achievement-sound.ts @@ -36,4 +36,3 @@ const removeThemeAchievementSound = async ( }; registerEvent("removeThemeAchievementSound", removeThemeAchievementSound); - diff --git a/src/main/services/notifications/index.ts b/src/main/services/notifications/index.ts index d78a4d3f..f1df1478 100644 --- a/src/main/services/notifications/index.ts +++ b/src/main/services/notifications/index.ts @@ -5,7 +5,7 @@ import fs from "node:fs"; import axios from "axios"; import path from "node:path"; import sound from "sound-play"; -import { achievementSoundPath, DEFAULT_ACHIEVEMENT_SOUND_VOLUME } from "@main/constants"; +import { achievementSoundPath } from "@main/constants"; import icon from "@resources/icon.png?asset"; import { NotificationOptions, toXmlString } from "./xml"; import { logger } from "../logger"; @@ -59,22 +59,6 @@ async function getAchievementSoundPath(): Promise { return achievementSoundPath; } -async function getAchievementSoundVolume(): Promise { - try { - const userPreferences = await db.get( - levelKeys.userPreferences, - { - valueEncoding: "json", - } - ); - - return userPreferences?.achievementSoundVolume ?? DEFAULT_ACHIEVEMENT_SOUND_VOLUME; - } catch (error) { - logger.error("Failed to get achievement sound volume", error); - return DEFAULT_ACHIEVEMENT_SOUND_VOLUME; - } -} - export const publishDownloadCompleteNotification = async (game: Game) => { const userPreferences = await db.get( levelKeys.userPreferences, diff --git a/src/preload/index.ts b/src/preload/index.ts index 67ac0c73..951591a5 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -577,8 +577,17 @@ contextBridge.exposeInMainWorld("electron", { ipcRenderer.invoke("removeThemeAchievementSound", themeId), getThemeSoundPath: (themeId: string) => ipcRenderer.invoke("getThemeSoundPath", themeId), - importThemeSoundFromStore: (themeId: string, themeName: string, storeUrl: string) => - ipcRenderer.invoke("importThemeSoundFromStore", themeId, themeName, storeUrl), + importThemeSoundFromStore: ( + themeId: string, + themeName: string, + storeUrl: string + ) => + ipcRenderer.invoke( + "importThemeSoundFromStore", + themeId, + themeName, + storeUrl + ), /* Editor */ openEditorWindow: (themeId: string) => diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index fd1c2735..272551ad 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -24,7 +24,12 @@ import { UserFriendModal } from "./pages/shared-modals/user-friend-modal"; import { useSubscription } from "./hooks/use-subscription"; import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal"; -import { injectCustomCss, removeCustomCss, getAchievementSoundUrl, getAchievementSoundVolume } from "./helpers"; +import { + injectCustomCss, + removeCustomCss, + getAchievementSoundUrl, + getAchievementSoundVolume, +} from "./helpers"; import "./app.scss"; export interface AppProps { diff --git a/src/renderer/src/helpers.ts b/src/renderer/src/helpers.ts index 3ee04805..d5326de0 100644 --- a/src/renderer/src/helpers.ts +++ b/src/renderer/src/helpers.ts @@ -123,11 +123,12 @@ export const generateUUID = (): string => { }; export const getAchievementSoundUrl = async (): Promise => { - const defaultSound = (await import("@renderer/assets/audio/achievement.wav")).default; - + const defaultSound = (await import("@renderer/assets/audio/achievement.wav")) + .default; + try { const activeTheme = await window.electron.getActiveCustomTheme(); - + if (activeTheme?.hasCustomSound) { const soundPath = await window.electron.getThemeSoundPath(activeTheme.id); if (soundPath) { @@ -137,7 +138,7 @@ export const getAchievementSoundUrl = async (): Promise => { } catch (error) { console.error("Failed to get theme sound", error); } - + return defaultSound; }; diff --git a/src/renderer/src/pages/achievements/notification/achievement-notification.tsx b/src/renderer/src/pages/achievements/notification/achievement-notification.tsx index 02023f1d..38b2443b 100644 --- a/src/renderer/src/pages/achievements/notification/achievement-notification.tsx +++ b/src/renderer/src/pages/achievements/notification/achievement-notification.tsx @@ -4,7 +4,12 @@ import { AchievementCustomNotificationPosition, AchievementNotificationInfo, } from "@types"; -import { injectCustomCss, removeCustomCss, getAchievementSoundUrl, getAchievementSoundVolume } from "@renderer/helpers"; +import { + injectCustomCss, + removeCustomCss, + getAchievementSoundUrl, + getAchievementSoundVolume, +} from "@renderer/helpers"; import { AchievementNotificationItem } from "@renderer/components/achievements/notification/achievement-notification"; import app from "../../../app.scss?inline"; import styles from "../../../components/achievements/notification/achievement-notification.scss?inline"; diff --git a/src/renderer/src/pages/settings/settings-general.tsx b/src/renderer/src/pages/settings/settings-general.tsx index 6d81f763..172b3291 100644 --- a/src/renderer/src/pages/settings/settings-general.tsx +++ b/src/renderer/src/pages/settings/settings-general.tsx @@ -1,4 +1,11 @@ -import { useContext, useEffect, useMemo, useState, useCallback, useRef } from "react"; +import { + useContext, + useEffect, + useMemo, + useState, + useCallback, + useRef, +} from "react"; import { TextField, Button, @@ -51,7 +58,7 @@ export function SettingsGeneral() { const [languageOptions, setLanguageOptions] = useState([]); const [defaultDownloadsPath, setDefaultDownloadsPath] = useState(""); - + const volumeUpdateTimeoutRef = useRef(); useEffect(() => { @@ -116,7 +123,9 @@ export function SettingsGeneral() { userPreferences.achievementCustomNotificationsEnabled ?? true, achievementCustomNotificationPosition: userPreferences.achievementCustomNotificationPosition ?? "top-left", - achievementSoundVolume: Math.round((userPreferences.achievementSoundVolume ?? 0.15) * 100), + achievementSoundVolume: Math.round( + (userPreferences.achievementSoundVolume ?? 0.15) * 100 + ), friendRequestNotificationsEnabled: userPreferences.friendRequestNotificationsEnabled ?? false, friendStartGameNotificationsEnabled: @@ -155,17 +164,20 @@ export function SettingsGeneral() { await updateUserPreferences(values); }; - const handleVolumeChange = useCallback((newVolume: number) => { - setForm((prev) => ({ ...prev, achievementSoundVolume: newVolume })); - - if (volumeUpdateTimeoutRef.current) { - clearTimeout(volumeUpdateTimeoutRef.current); - } - - volumeUpdateTimeoutRef.current = setTimeout(() => { - updateUserPreferences({ achievementSoundVolume: newVolume / 100 }); - }, 300); - }, [updateUserPreferences]); + const handleVolumeChange = useCallback( + (newVolume: number) => { + setForm((prev) => ({ ...prev, achievementSoundVolume: newVolume })); + + if (volumeUpdateTimeoutRef.current) { + clearTimeout(volumeUpdateTimeoutRef.current); + } + + volumeUpdateTimeoutRef.current = setTimeout(() => { + updateUserPreferences({ achievementSoundVolume: newVolume / 100 }); + }, 300); + }, + [updateUserPreferences] + ); const handleChangeAchievementCustomNotificationPosition = async ( event: React.ChangeEvent @@ -347,13 +359,19 @@ export function SettingsGeneral() { handleVolumeChange(0); return; } - const volumePercent = Math.min(100, Math.max(0, parseInt(value, 10))); + const volumePercent = Math.min( + 100, + Math.max(0, parseInt(value, 10)) + ); if (!isNaN(volumePercent)) { handleVolumeChange(volumePercent); } }} onBlur={(e) => { - if (e.target.value === "" || isNaN(parseInt(e.target.value, 10))) { + if ( + e.target.value === "" || + isNaN(parseInt(e.target.value, 10)) + ) { handleVolumeChange(0); } }} @@ -365,11 +383,19 @@ export function SettingsGeneral() { type="button" onClick={(e) => { e.preventDefault(); - const newVolume = Math.min(100, form.achievementSoundVolume + 1); + const newVolume = Math.min( + 100, + form.achievementSoundVolume + 1 + ); handleVolumeChange(newVolume); }} > - + @@ -377,11 +403,19 @@ export function SettingsGeneral() { type="button" onClick={(e) => { e.preventDefault(); - const newVolume = Math.max(0, form.achievementSoundVolume - 1); + const newVolume = Math.max( + 0, + form.achievementSoundVolume - 1 + ); handleVolumeChange(newVolume); }} > - + diff --git a/src/renderer/src/pages/theme-editor/theme-editor.tsx b/src/renderer/src/pages/theme-editor/theme-editor.tsx index 8ae86f63..6057084f 100644 --- a/src/renderer/src/pages/theme-editor/theme-editor.tsx +++ b/src/renderer/src/pages/theme-editor/theme-editor.tsx @@ -4,10 +4,19 @@ import Editor from "@monaco-editor/react"; import { AchievementCustomNotificationPosition, Theme } from "@types"; import { useSearchParams } from "react-router-dom"; import { Button, SelectField } from "@renderer/components"; -import { CheckIcon, UploadIcon, TrashIcon, PlayIcon } from "@primer/octicons-react"; +import { + CheckIcon, + UploadIcon, + TrashIcon, + PlayIcon, +} from "@primer/octicons-react"; import { useTranslation } from "react-i18next"; import cn from "classnames"; -import { injectCustomCss, getAchievementSoundUrl, getAchievementSoundVolume } from "@renderer/helpers"; +import { + injectCustomCss, + getAchievementSoundUrl, + getAchievementSoundVolume, +} from "@renderer/helpers"; import { AchievementNotificationItem } from "@renderer/components/achievements/notification/achievement-notification"; import { generateAchievementCustomNotificationTest } from "@shared"; import { CollapsedMenu } from "@renderer/components/collapsed-menu/collapsed-menu"; From b6bbf05da6115de40589b6d8b346eef2af9824cf Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 7 Nov 2025 20:12:50 +0200 Subject: [PATCH 3/9] fix: theme editor layout positioning --- src/main/events/index.ts | 1 + .../events/themes/get-theme-sound-data-url.ts | 38 ++++++++ src/preload/index.ts | 2 + src/renderer/src/declaration.d.ts | 1 + src/renderer/src/helpers.ts | 8 +- .../src/pages/theme-editor/theme-editor.scss | 26 ++++- .../src/pages/theme-editor/theme-editor.tsx | 94 ++++++++++--------- 7 files changed, 116 insertions(+), 54 deletions(-) create mode 100644 src/main/events/themes/get-theme-sound-data-url.ts diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 89dd01f5..834f47ba 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -95,6 +95,7 @@ import "./themes/toggle-custom-theme"; import "./themes/copy-theme-achievement-sound"; import "./themes/remove-theme-achievement-sound"; import "./themes/get-theme-sound-path"; +import "./themes/get-theme-sound-data-url"; import "./themes/import-theme-sound-from-store"; import "./download-sources/remove-download-source"; import "./download-sources/get-download-sources"; diff --git a/src/main/events/themes/get-theme-sound-data-url.ts b/src/main/events/themes/get-theme-sound-data-url.ts new file mode 100644 index 00000000..b9ace306 --- /dev/null +++ b/src/main/events/themes/get-theme-sound-data-url.ts @@ -0,0 +1,38 @@ +import { registerEvent } from "../register-event"; +import { getThemeSoundPath } from "@main/helpers"; +import fs from "node:fs"; +import path from "node:path"; +import { logger } from "@main/services"; + +const getThemeSoundDataUrl = async ( + _event: Electron.IpcMainInvokeEvent, + themeId: string +): Promise => { + try { + const soundPath = getThemeSoundPath(themeId); + + if (!soundPath || !fs.existsSync(soundPath)) { + return null; + } + + const buffer = await fs.promises.readFile(soundPath); + const ext = path.extname(soundPath).toLowerCase().slice(1); + + const mimeTypes: Record = { + mp3: "audio/mpeg", + wav: "audio/wav", + ogg: "audio/ogg", + m4a: "audio/mp4", + }; + + const mimeType = mimeTypes[ext] || "audio/mpeg"; + const base64 = buffer.toString("base64"); + + return `data:${mimeType};base64,${base64}`; + } catch (error) { + logger.error("Failed to get theme sound data URL", error); + return null; + } +}; + +registerEvent("getThemeSoundDataUrl", getThemeSoundDataUrl); diff --git a/src/preload/index.ts b/src/preload/index.ts index 951591a5..bfb1de6e 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -577,6 +577,8 @@ contextBridge.exposeInMainWorld("electron", { ipcRenderer.invoke("removeThemeAchievementSound", themeId), getThemeSoundPath: (themeId: string) => ipcRenderer.invoke("getThemeSoundPath", themeId), + getThemeSoundDataUrl: (themeId: string) => + ipcRenderer.invoke("getThemeSoundDataUrl", themeId), importThemeSoundFromStore: ( themeId: string, themeName: string, diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 44696872..9eefb477 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -416,6 +416,7 @@ declare global { ) => Promise; removeThemeAchievementSound: (themeId: string) => Promise; getThemeSoundPath: (themeId: string) => Promise; + getThemeSoundDataUrl: (themeId: string) => Promise; importThemeSoundFromStore: ( themeId: string, themeName: string, diff --git a/src/renderer/src/helpers.ts b/src/renderer/src/helpers.ts index d5326de0..e16aa7a4 100644 --- a/src/renderer/src/helpers.ts +++ b/src/renderer/src/helpers.ts @@ -130,9 +130,11 @@ export const getAchievementSoundUrl = async (): Promise => { const activeTheme = await window.electron.getActiveCustomTheme(); if (activeTheme?.hasCustomSound) { - const soundPath = await window.electron.getThemeSoundPath(activeTheme.id); - if (soundPath) { - return `file://${soundPath}`; + const soundDataUrl = await window.electron.getThemeSoundDataUrl( + activeTheme.id + ); + if (soundDataUrl) { + return soundDataUrl; } } } catch (error) { diff --git a/src/renderer/src/pages/theme-editor/theme-editor.scss b/src/renderer/src/pages/theme-editor/theme-editor.scss index b34217f9..2d3d9067 100644 --- a/src/renderer/src/pages/theme-editor/theme-editor.scss +++ b/src/renderer/src/pages/theme-editor/theme-editor.scss @@ -47,6 +47,8 @@ position: relative; border: 1px solid globals.$muted-color; border-radius: 2px; + flex: 1; + min-width: 0; } &__footer { @@ -80,7 +82,7 @@ } &__info { - padding: 16px; + padding: 8px; p { font-size: 16px; @@ -93,25 +95,39 @@ &__notification-preview { padding-top: 12px; display: flex; - flex-direction: column; + flex-direction: row; gap: 16px; + align-items: flex-start; &__select-variation { flex: inherit; } } + &__notification-preview-controls { + display: flex; + flex-direction: column; + gap: 16px; + flex-shrink: 0; + } + &__notification-controls { display: flex; flex-direction: row; align-items: center; - gap: 16px; + gap: 8px; } &__sound-controls { display: flex; - flex-direction: row; + flex-direction: column; gap: 8px; - flex-wrap: wrap; + width: fit-content; + + button, + .button { + width: auto; + align-self: flex-start; + } } } diff --git a/src/renderer/src/pages/theme-editor/theme-editor.tsx b/src/renderer/src/pages/theme-editor/theme-editor.tsx index 6057084f..75df5e1e 100644 --- a/src/renderer/src/pages/theme-editor/theme-editor.tsx +++ b/src/renderer/src/pages/theme-editor/theme-editor.tsx @@ -213,57 +213,59 @@ export default function ThemeEditor() {
-
- { - return { - key: variation, - value: variation, - label: t(variation), - }; +
+
+ { + return { + key: variation, + value: variation, + label: t(variation), + }; + } + )} + onChange={(value) => + setNotificationVariation( + value.target.value as keyof typeof notificationVariations + ) } - )} - onChange={(value) => - setNotificationVariation( - value.target.value as keyof typeof notificationVariations - ) - } - /> + /> - - setNotificationAlignment( - e.target.value as AchievementCustomNotificationPosition - ) - } - options={achievementCustomNotificationPositionOptions} - /> -
+ + setNotificationAlignment( + e.target.value as AchievementCustomNotificationPosition + ) + } + options={achievementCustomNotificationPositionOptions} + /> +
-
- - - {theme?.hasCustomSound && ( - - )} - + {theme?.hasCustomSound && ( + + )} + + +
From 482d9b2f96bada5fb42c35e7b07e2189912bd22b Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 8 Nov 2025 15:14:12 +0200 Subject: [PATCH 4/9] fix: ensure consistent custom sound detection across main and renderer processes --- src/main/services/notifications/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/services/notifications/index.ts b/src/main/services/notifications/index.ts index f1df1478..926ba47a 100644 --- a/src/main/services/notifications/index.ts +++ b/src/main/services/notifications/index.ts @@ -46,7 +46,7 @@ async function getAchievementSoundPath(): Promise { const allThemes = await themesSublevel.values().all(); const activeTheme = allThemes.find((theme) => theme.isActive); - if (activeTheme) { + if (activeTheme?.hasCustomSound) { const themeSoundPath = getThemeSoundPath(activeTheme.id); if (themeSoundPath) { return themeSoundPath; From 3daf28c8825250c6f9ca15138d2ac7e63590e782 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sun, 9 Nov 2025 04:19:52 +0200 Subject: [PATCH 5/9] fix: handling exception and ESLint issues --- src/main/events/themes/import-theme-sound-from-store.ts | 4 ++++ src/renderer/src/pages/settings/settings-general.tsx | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/events/themes/import-theme-sound-from-store.ts b/src/main/events/themes/import-theme-sound-from-store.ts index 588db6f5..135353b2 100644 --- a/src/main/events/themes/import-theme-sound-from-store.ts +++ b/src/main/events/themes/import-theme-sound-from-store.ts @@ -46,6 +46,10 @@ const importThemeSoundFromStore = async ( logger.log(`Successfully imported sound for theme ${themeName}`); return; } catch (error) { + logger.error( + `Failed to import ${format} sound for theme ${themeName}`, + error + ); continue; } } diff --git a/src/renderer/src/pages/settings/settings-general.tsx b/src/renderer/src/pages/settings/settings-general.tsx index 172b3291..c57fb5b3 100644 --- a/src/renderer/src/pages/settings/settings-general.tsx +++ b/src/renderer/src/pages/settings/settings-general.tsx @@ -361,16 +361,16 @@ export function SettingsGeneral() { } const volumePercent = Math.min( 100, - Math.max(0, parseInt(value, 10)) + Math.max(0, Number.parseInt(value, 10)) ); - if (!isNaN(volumePercent)) { + if (!Number.isNaN(volumePercent)) { handleVolumeChange(volumePercent); } }} onBlur={(e) => { if ( e.target.value === "" || - isNaN(parseInt(e.target.value, 10)) + Number.isNaN(Number.parseInt(e.target.value, 10)) ) { handleVolumeChange(0); } From e272470a7b6549333b6a379f6950911bf995f19f Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sun, 9 Nov 2025 15:28:52 +0200 Subject: [PATCH 6/9] feat: using theme name for folder instead themeid --- .../themes/copy-theme-achievement-sound.ts | 2 +- .../events/themes/get-theme-sound-data-url.ts | 4 +- .../events/themes/get-theme-sound-path.ts | 4 +- .../themes/import-theme-sound-from-store.ts | 2 +- .../themes/remove-theme-achievement-sound.ts | 31 ++++++---- src/main/helpers/index.ts | 57 +++++++++++++++---- src/main/services/notifications/index.ts | 5 +- 7 files changed, 77 insertions(+), 28 deletions(-) diff --git a/src/main/events/themes/copy-theme-achievement-sound.ts b/src/main/events/themes/copy-theme-achievement-sound.ts index aec22cb2..e2c927fd 100644 --- a/src/main/events/themes/copy-theme-achievement-sound.ts +++ b/src/main/events/themes/copy-theme-achievement-sound.ts @@ -18,7 +18,7 @@ const copyThemeAchievementSound = async ( throw new Error("Theme not found"); } - const themeDir = getThemePath(themeId); + const themeDir = getThemePath(themeId, theme.name); if (!fs.existsSync(themeDir)) { fs.mkdirSync(themeDir, { recursive: true }); diff --git a/src/main/events/themes/get-theme-sound-data-url.ts b/src/main/events/themes/get-theme-sound-data-url.ts index b9ace306..a93538dd 100644 --- a/src/main/events/themes/get-theme-sound-data-url.ts +++ b/src/main/events/themes/get-theme-sound-data-url.ts @@ -1,5 +1,6 @@ import { registerEvent } from "../register-event"; import { getThemeSoundPath } from "@main/helpers"; +import { themesSublevel } from "@main/level"; import fs from "node:fs"; import path from "node:path"; import { logger } from "@main/services"; @@ -9,7 +10,8 @@ const getThemeSoundDataUrl = async ( themeId: string ): Promise => { try { - const soundPath = getThemeSoundPath(themeId); + const theme = await themesSublevel.get(themeId); + const soundPath = getThemeSoundPath(themeId, theme?.name); if (!soundPath || !fs.existsSync(soundPath)) { return null; diff --git a/src/main/events/themes/get-theme-sound-path.ts b/src/main/events/themes/get-theme-sound-path.ts index 37783949..11658c6a 100644 --- a/src/main/events/themes/get-theme-sound-path.ts +++ b/src/main/events/themes/get-theme-sound-path.ts @@ -1,11 +1,13 @@ import { registerEvent } from "../register-event"; import { getThemeSoundPath } from "@main/helpers"; +import { themesSublevel } from "@main/level"; const getThemeSoundPathEvent = async ( _event: Electron.IpcMainInvokeEvent, themeId: string ): Promise => { - return getThemeSoundPath(themeId); + const theme = await themesSublevel.get(themeId); + return getThemeSoundPath(themeId, theme?.name); }; registerEvent("getThemeSoundPath", getThemeSoundPathEvent); diff --git a/src/main/events/themes/import-theme-sound-from-store.ts b/src/main/events/themes/import-theme-sound-from-store.ts index 135353b2..66da6cb3 100644 --- a/src/main/events/themes/import-theme-sound-from-store.ts +++ b/src/main/events/themes/import-theme-sound-from-store.ts @@ -28,7 +28,7 @@ const importThemeSoundFromStore = async ( timeout: 10000, }); - const themeDir = getThemePath(themeId); + const themeDir = getThemePath(themeId, theme.name); if (!fs.existsSync(themeDir)) { fs.mkdirSync(themeDir, { recursive: true }); diff --git a/src/main/events/themes/remove-theme-achievement-sound.ts b/src/main/events/themes/remove-theme-achievement-sound.ts index 4ca738f3..6c17bb6f 100644 --- a/src/main/events/themes/remove-theme-achievement-sound.ts +++ b/src/main/events/themes/remove-theme-achievement-sound.ts @@ -2,6 +2,7 @@ import { registerEvent } from "../register-event"; import fs from "node:fs"; import { getThemePath } from "@main/helpers"; import { themesSublevel } from "@main/level"; +import { THEMES_PATH } from "@main/constants"; import path from "node:path"; const removeThemeAchievementSound = async ( @@ -13,19 +14,27 @@ const removeThemeAchievementSound = async ( throw new Error("Theme not found"); } - const themeDir = getThemePath(themeId); + const themeDir = getThemePath(themeId, theme.name); + const legacyThemeDir = path.join(THEMES_PATH, themeId); - if (!fs.existsSync(themeDir)) { - return; - } - - const formats = ["wav", "mp3", "ogg", "m4a"]; - - for (const format of formats) { - const soundPath = path.join(themeDir, `achievement.${format}`); - if (fs.existsSync(soundPath)) { - await fs.promises.unlink(soundPath); + const removeFromDir = async (dir: string) => { + if (!fs.existsSync(dir)) { + return; } + + const formats = ["wav", "mp3", "ogg", "m4a"]; + + for (const format of formats) { + const soundPath = path.join(dir, `achievement.${format}`); + if (fs.existsSync(soundPath)) { + await fs.promises.unlink(soundPath); + } + } + }; + + await removeFromDir(themeDir); + if (themeDir !== legacyThemeDir) { + await removeFromDir(legacyThemeDir); } await themesSublevel.put(themeId, { diff --git a/src/main/helpers/index.ts b/src/main/helpers/index.ts index ae19fbdb..550db81f 100644 --- a/src/main/helpers/index.ts +++ b/src/main/helpers/index.ts @@ -38,23 +38,56 @@ export const normalizePath = (str: string) => export const addTrailingSlash = (str: string) => str.endsWith("/") ? str : `${str}/`; -export const getThemePath = (themeId: string) => - path.join(THEMES_PATH, themeId); +const sanitizeFolderName = (name: string): string => { + return name + .toLowerCase() + .replace(/[^a-z0-9-_\s]/g, "") + .replace(/\s+/g, "-") + .replace(/-+/g, "-") + .replace(/^-|-$/g, ""); +}; -export const getThemeSoundPath = (themeId: string): string | null => { - const themeDir = getThemePath(themeId); +export const getThemePath = (themeId: string, themeName?: string): string => { + if (themeName) { + const sanitizedName = sanitizeFolderName(themeName); + if (sanitizedName) { + return path.join(THEMES_PATH, sanitizedName); + } + } + return path.join(THEMES_PATH, themeId); +}; + +export const getThemeSoundPath = ( + themeId: string, + themeName?: string +): string | null => { + const themeDir = getThemePath(themeId, themeName); + const legacyThemeDir = themeName ? path.join(THEMES_PATH, themeId) : null; + + const checkDir = (dir: string): string | null => { + if (!fs.existsSync(dir)) { + return null; + } + + const formats = ["wav", "mp3", "ogg", "m4a"]; + + for (const format of formats) { + const soundPath = path.join(dir, `achievement.${format}`); + if (fs.existsSync(soundPath)) { + return soundPath; + } + } - if (!fs.existsSync(themeDir)) { return null; + }; + + const soundPath = checkDir(themeDir); + if (soundPath) { + return soundPath; } - const formats = ["wav", "mp3", "ogg", "m4a"]; - - for (const format of formats) { - const soundPath = path.join(themeDir, `achievement.${format}`); - if (fs.existsSync(soundPath)) { - return soundPath; - } + if (legacyThemeDir) { + return checkDir(legacyThemeDir); } return null; diff --git a/src/main/services/notifications/index.ts b/src/main/services/notifications/index.ts index 926ba47a..b8ff480c 100644 --- a/src/main/services/notifications/index.ts +++ b/src/main/services/notifications/index.ts @@ -47,7 +47,10 @@ async function getAchievementSoundPath(): Promise { const activeTheme = allThemes.find((theme) => theme.isActive); if (activeTheme?.hasCustomSound) { - const themeSoundPath = getThemeSoundPath(activeTheme.id); + const themeSoundPath = getThemeSoundPath( + activeTheme.id, + activeTheme.name + ); if (themeSoundPath) { return themeSoundPath; } From d54ff9a949c1fc4be63f02eda3fd4b917f8149ca Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sun, 9 Nov 2025 15:34:24 +0200 Subject: [PATCH 7/9] fix: eslint issues --- src/main/helpers/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/helpers/index.ts b/src/main/helpers/index.ts index 550db81f..664dbd78 100644 --- a/src/main/helpers/index.ts +++ b/src/main/helpers/index.ts @@ -33,7 +33,7 @@ export const isPortableVersion = () => { }; export const normalizePath = (str: string) => - path.posix.normalize(str).replace(/\\/g, "/"); + path.posix.normalize(str).replaceAll("\\", "/"); export const addTrailingSlash = (str: string) => str.endsWith("/") ? str : `${str}/`; @@ -41,10 +41,10 @@ export const addTrailingSlash = (str: string) => const sanitizeFolderName = (name: string): string => { return name .toLowerCase() - .replace(/[^a-z0-9-_\s]/g, "") - .replace(/\s+/g, "-") - .replace(/-+/g, "-") - .replace(/^-|-$/g, ""); + .replaceAll(/[^a-z0-9-_\s]/g, "") + .replaceAll(/\s+/g, "-") + .replaceAll(/-+/g, "-") + .replaceAll(/(^-|-$)/g, ""); }; export const getThemePath = (themeId: string, themeName?: string): string => { From 399669a94cfd670502ccfd281379cf55d9588098 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Mon, 10 Nov 2025 22:55:17 +0000 Subject: [PATCH 8/9] feat: adding slider to achievement sound --- src/locales/en/translation.json | 4 + src/locales/es/translation.json | 6 + src/locales/pt-BR/translation.json | 6 + src/locales/ru/translation.json | 6 + .../themes/copy-theme-achievement-sound.ts | 1 + .../themes/remove-theme-achievement-sound.ts | 1 + .../src/pages/settings/settings-general.scss | 187 ++++++++++++------ .../src/pages/settings/settings-general.tsx | 101 +++------- .../src/pages/theme-editor/theme-editor.scss | 20 +- .../src/pages/theme-editor/theme-editor.tsx | 68 +++++-- src/types/theme.types.ts | 1 + 11 files changed, 233 insertions(+), 168 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 30b165ae..71631f91 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -562,6 +562,10 @@ "change_achievement_sound": "Change achievement sound", "remove_achievement_sound": "Remove achievement sound", "preview_sound": "Preview sound", + "select": "Select", + "preview": "Preview", + "remove": "Remove", + "no_sound_file_selected": "No sound file selected", "notification_preview": "Achievement Notification Preview", "enable_friend_start_game_notifications": "When a friend starts playing a game", "autoplay_trailers_on_game_page": "Automatically start playing trailers on game page", diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index c7e9d13e..12c19d01 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -542,6 +542,12 @@ "platinum": "Platino", "hidden": "Oculto", "test_notification": "Probar notificación", + "achievement_sound_volume": "Volumen del sonido de logro", + "select_achievement_sound": "Seleccionar sonido de logro", + "select": "Seleccionar", + "preview": "Vista previa", + "remove": "Remover", + "no_sound_file_selected": "No se seleccionó ningún archivo de sonido", "notification_preview": "Probar notificación de logro", "debrid": "Debrid", "debrid_description": "Los servicios Debrid son descargadores premium sin restricciones que te dejan descargar más rápido archivos alojados en servicios de alojamiento siendo que la única limitación es tu velocidad de internet.", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index 50049140..90346b22 100755 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -541,6 +541,12 @@ "platinum": "Platina", "hidden": "Oculta", "test_notification": "Testar notificação", + "achievement_sound_volume": "Volume do som de conquista", + "select_achievement_sound": "Selecionar som de conquista", + "select": "Selecionar", + "preview": "Reproduzir", + "remove": "Remover", + "no_sound_file_selected": "Nenhum arquivo de som selecionado", "notification_preview": "Prévia da Notificação de Conquistas", "enable_friend_start_game_notifications": "Quando um amigo iniciar um jogo", "autoplay_trailers_on_game_page": "Reproduzir trailers automaticamente na página do jogo", diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index 2e7c1504..f210d96f 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -555,6 +555,12 @@ "platinum": "Платиновый", "hidden": "Скрытый", "test_notification": "Тестовое уведомление", + "achievement_sound_volume": "Громкость звука достижения", + "select_achievement_sound": "Выбрать звук достижения", + "select": "Выбрать", + "preview": "Предпросмотр", + "remove": "Удалить", + "no_sound_file_selected": "Файл звука не выбран", "notification_preview": "Предварительный просмотр уведомления о достижении", "enable_friend_start_game_notifications": "Когда друг начинает играть в игру", "autoplay_trailers_on_game_page": "Автоматически начинать воспроизведение трейлеров на странице игры", diff --git a/src/main/events/themes/copy-theme-achievement-sound.ts b/src/main/events/themes/copy-theme-achievement-sound.ts index aec22cb2..2ec10198 100644 --- a/src/main/events/themes/copy-theme-achievement-sound.ts +++ b/src/main/events/themes/copy-theme-achievement-sound.ts @@ -32,6 +32,7 @@ const copyThemeAchievementSound = async ( await themesSublevel.put(themeId, { ...theme, hasCustomSound: true, + originalSoundPath: sourcePath, updatedAt: new Date(), }); }; diff --git a/src/main/events/themes/remove-theme-achievement-sound.ts b/src/main/events/themes/remove-theme-achievement-sound.ts index 4ca738f3..16500a11 100644 --- a/src/main/events/themes/remove-theme-achievement-sound.ts +++ b/src/main/events/themes/remove-theme-achievement-sound.ts @@ -31,6 +31,7 @@ const removeThemeAchievementSound = async ( await themesSublevel.put(themeId, { ...theme, hasCustomSound: false, + originalSoundPath: undefined, updatedAt: new Date(), }); }; diff --git a/src/renderer/src/pages/settings/settings-general.scss b/src/renderer/src/pages/settings/settings-general.scss index 58004362..8a6a0ac1 100644 --- a/src/renderer/src/pages/settings/settings-general.scss +++ b/src/renderer/src/pages/settings/settings-general.scss @@ -21,7 +21,7 @@ &__volume-control { display: flex; flex-direction: column; - gap: 8px; + gap: 12px; label { font-size: 14px; @@ -29,88 +29,147 @@ } } - &__volume-input-wrapper { + &__volume-slider-wrapper { display: flex; align-items: center; - gap: 4px; - } - - &__volume-input-container { + gap: 8px; + width: 200px; position: relative; - display: flex; - align-items: center; - background: globals.$dark-background-color; - border: 1px solid globals.$border-color; - padding: 8px 8px; - border-radius: 4px; - transition: border-color 0.2s; - - &:focus-within { - border-color: rgba(255, 255, 255, 0.5); - color: globals.$muted-color; - } - - input[type="number"] { - width: 30px; - background: transparent; - border: none; - color: globals.$muted-color; - font-size: 14px; - text-align: center; - - &:focus { - outline: none; - } - - &::-webkit-inner-spin-button, - &::-webkit-outer-spin-button { - -webkit-appearance: none; - margin: 0; - } - - appearance: textfield; - -moz-appearance: textfield; - } + --volume-percent: 0%; } - &__volume-input-unit { - font-size: 14px; + &__volume-icon { color: globals.$muted-color; - pointer-events: none; - user-select: none; + flex-shrink: 0; } - &__volume-input-buttons { - display: flex; - flex-direction: column; - gap: 2px; + &__volume-value { + font-size: 14px; + color: globals.$body-color; + font-weight: 500; + min-width: 40px; + text-align: right; + flex-shrink: 0; + } - button { - display: flex; - align-items: center; - justify-content: center; - width: 24px; - height: 16px; - background: globals.$dark-background-color; - border: 1px solid globals.$border-color; - border-radius: 2px; - color: globals.$muted-color; + &__volume-slider { + flex: 1; + height: 6px; + border-radius: 3px; + background: globals.$dark-background-color; + outline: none; + -webkit-appearance: none; + appearance: none; + cursor: pointer; + transition: background 0.2s; + + &::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 18px; + height: 18px; + border-radius: 50%; + background: globals.$muted-color; cursor: pointer; + border: 2px solid globals.$background-color; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); transition: all 0.2s; + margin-top: -6px; &:hover { - color: globals.$muted-color; - border-color: rgba(255, 255, 255, 0.5); + transform: scale(1.1); + box-shadow: 0 2px 6px rgba(255, 255, 255, 0.4); } &:active { - background: globals.$background-color; + transform: scale(1.05); + } + } + + &::-moz-range-thumb { + width: 18px; + height: 18px; + border-radius: 50%; + background: globals.$muted-color; + cursor: pointer; + border: 2px solid globals.$background-color; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); + transition: all 0.2s; + margin-top: -6px; + + &:hover { + transform: scale(1.1); + box-shadow: 0 2px 6px rgba(255, 255, 255, 0.4); } - svg { - width: 12px; - height: 12px; + &:active { + transform: scale(1.05); } } + + &::-webkit-slider-runnable-track { + width: 100%; + height: 6px; + border-radius: 3px; + background: linear-gradient( + to right, + globals.$muted-color 0%, + globals.$muted-color var(--volume-percent), + globals.$dark-background-color var(--volume-percent), + globals.$dark-background-color 100% + ); + } + + &::-moz-range-track { + width: 100%; + height: 6px; + border-radius: 3px; + background: globals.$dark-background-color; + } + + &::-moz-range-progress { + height: 6px; + border-radius: 3px; + background: globals.$muted-color; + } + + &:focus { + outline: none; + + &::-webkit-slider-thumb { + box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.2); + } + + &::-moz-range-thumb { + box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.2); + } + } + + &::-ms-thumb { + width: 18px; + height: 18px; + border-radius: 50%; + background: globals.$muted-color; + cursor: pointer; + border: 2px solid globals.$background-color; + } + + &::-ms-track { + width: 100%; + height: 6px; + background: transparent; + border-color: transparent; + color: transparent; + } + + &::-ms-fill-lower { + background: globals.$muted-color; + border-radius: 3px; + } + + &::-ms-fill-upper { + background: globals.$dark-background-color; + border-radius: 3px; + } } } diff --git a/src/renderer/src/pages/settings/settings-general.tsx b/src/renderer/src/pages/settings/settings-general.tsx index 172b3291..c81ced7d 100644 --- a/src/renderer/src/pages/settings/settings-general.tsx +++ b/src/renderer/src/pages/settings/settings-general.tsx @@ -19,7 +19,7 @@ import languageResources from "@locales"; import { orderBy } from "lodash-es"; import { settingsContext } from "@renderer/context"; import "./settings-general.scss"; -import { DesktopDownloadIcon } from "@primer/octicons-react"; +import { DesktopDownloadIcon, UnmuteIcon } from "@primer/octicons-react"; import { logger } from "@renderer/logger"; import { AchievementCustomNotificationPosition } from "@types"; @@ -345,81 +345,30 @@ export function SettingsGeneral() { -
-
- { - const value = e.target.value; - if (value === "") { - handleVolumeChange(0); - return; - } - const volumePercent = Math.min( - 100, - Math.max(0, parseInt(value, 10)) - ); - if (!isNaN(volumePercent)) { - handleVolumeChange(volumePercent); - } - }} - onBlur={(e) => { - if ( - e.target.value === "" || - isNaN(parseInt(e.target.value, 10)) - ) { - handleVolumeChange(0); - } - }} - /> - % -
-
- - -
+
+ + { + const volumePercent = parseInt(e.target.value, 10); + if (!isNaN(volumePercent)) { + handleVolumeChange(volumePercent); + } + }} + className="settings-general__volume-slider" + style={ + { + "--volume-percent": `${form.achievementSoundVolume}%`, + } as React.CSSProperties + } + /> + + {form.achievementSoundVolume}% +
)} diff --git a/src/renderer/src/pages/theme-editor/theme-editor.scss b/src/renderer/src/pages/theme-editor/theme-editor.scss index 2d3d9067..486f694c 100644 --- a/src/renderer/src/pages/theme-editor/theme-editor.scss +++ b/src/renderer/src/pages/theme-editor/theme-editor.scss @@ -95,9 +95,8 @@ &__notification-preview { padding-top: 12px; display: flex; - flex-direction: row; + flex-direction: column; gap: 16px; - align-items: flex-start; &__select-variation { flex: inherit; @@ -118,16 +117,17 @@ gap: 8px; } - &__sound-controls { + &__sound-actions { display: flex; - flex-direction: column; + flex-direction: row; gap: 8px; - width: fit-content; + align-items: center; + } - button, - .button { - width: auto; - align-self: flex-start; - } + &__sound-actions-row { + display: flex; + flex-direction: row; + gap: 8px; + align-items: center; } } diff --git a/src/renderer/src/pages/theme-editor/theme-editor.tsx b/src/renderer/src/pages/theme-editor/theme-editor.tsx index 75df5e1e..1224d4cd 100644 --- a/src/renderer/src/pages/theme-editor/theme-editor.tsx +++ b/src/renderer/src/pages/theme-editor/theme-editor.tsx @@ -3,7 +3,7 @@ import "./theme-editor.scss"; import Editor from "@monaco-editor/react"; import { AchievementCustomNotificationPosition, Theme } from "@types"; import { useSearchParams } from "react-router-dom"; -import { Button, SelectField } from "@renderer/components"; +import { Button, SelectField, TextField } from "@renderer/components"; import { CheckIcon, UploadIcon, @@ -14,7 +14,6 @@ import { useTranslation } from "react-i18next"; import cn from "classnames"; import { injectCustomCss, - getAchievementSoundUrl, getAchievementSoundVolume, } from "@renderer/helpers"; import { AchievementNotificationItem } from "@renderer/components/achievements/notification/achievement-notification"; @@ -36,6 +35,7 @@ export default function ThemeEditor() { const [theme, setTheme] = useState(null); const [code, setCode] = useState(""); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + const [soundPath, setSoundPath] = useState(""); const [isClosingNotifications, setIsClosingNotifications] = useState(false); @@ -71,6 +71,9 @@ export default function ThemeEditor() { if (loadedTheme) { setTheme(loadedTheme); setCode(loadedTheme.code); + if (loadedTheme.originalSoundPath) { + setSoundPath(loadedTheme.originalSoundPath); + } if (shadowRootRef) { injectCustomCss(loadedTheme.code, shadowRootRef); } @@ -130,10 +133,14 @@ export default function ThemeEditor() { }); if (filePaths && filePaths.length > 0) { - await window.electron.copyThemeAchievementSound(theme.id, filePaths[0]); + const originalPath = filePaths[0]; + await window.electron.copyThemeAchievementSound(theme.id, originalPath); const updatedTheme = await window.electron.getCustomThemeById(theme.id); if (updatedTheme) { setTheme(updatedTheme); + if (updatedTheme.originalSoundPath) { + setSoundPath(updatedTheme.originalSoundPath); + } } } }, [theme]); @@ -146,15 +153,34 @@ export default function ThemeEditor() { if (updatedTheme) { setTheme(updatedTheme); } + setSoundPath(""); }, [theme]); const handlePreviewSound = useCallback(async () => { - const soundUrl = await getAchievementSoundUrl(); + if (!theme) return; + + let soundUrl: string; + + if (theme.hasCustomSound) { + const themeSoundUrl = await window.electron.getThemeSoundDataUrl(theme.id); + if (themeSoundUrl) { + soundUrl = themeSoundUrl; + } else { + const defaultSound = (await import("@renderer/assets/audio/achievement.wav")) + .default; + soundUrl = defaultSound; + } + } else { + const defaultSound = (await import("@renderer/assets/audio/achievement.wav")) + .default; + soundUrl = defaultSound; + } + const volume = await getAchievementSoundVolume(); const audio = new Audio(soundUrl); audio.volume = volume; audio.play(); - }, []); + }, [theme]); const achievementCustomNotificationPositionOptions = useMemo(() => { return [ @@ -245,28 +271,34 @@ export default function ThemeEditor() { options={achievementCustomNotificationPositionOptions} />
+
-
+ - {theme?.hasCustomSound - ? t("change_achievement_sound") - : t("select_achievement_sound")} + {t("select")} + } + /> - {theme?.hasCustomSound && ( - - )} - + {theme?.hasCustomSound && ( +
+
-
+ )}
diff --git a/src/types/theme.types.ts b/src/types/theme.types.ts index 94285d9c..80976ec0 100644 --- a/src/types/theme.types.ts +++ b/src/types/theme.types.ts @@ -6,6 +6,7 @@ export interface Theme { isActive: boolean; code: string; hasCustomSound?: boolean; + originalSoundPath?: string; createdAt: Date; updatedAt: Date; } From 6fc5a70722c3bea8bf147416df9ff197a66d57ee Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Mon, 10 Nov 2025 22:55:49 +0000 Subject: [PATCH 9/9] feat: adding slider to achievement sound --- .../src/pages/theme-editor/theme-editor.tsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/renderer/src/pages/theme-editor/theme-editor.tsx b/src/renderer/src/pages/theme-editor/theme-editor.tsx index 1224d4cd..3f0be9cf 100644 --- a/src/renderer/src/pages/theme-editor/theme-editor.tsx +++ b/src/renderer/src/pages/theme-editor/theme-editor.tsx @@ -12,10 +12,7 @@ import { } from "@primer/octicons-react"; import { useTranslation } from "react-i18next"; import cn from "classnames"; -import { - injectCustomCss, - getAchievementSoundVolume, -} from "@renderer/helpers"; +import { injectCustomCss, getAchievementSoundVolume } from "@renderer/helpers"; import { AchievementNotificationItem } from "@renderer/components/achievements/notification/achievement-notification"; import { generateAchievementCustomNotificationTest } from "@shared"; import { CollapsedMenu } from "@renderer/components/collapsed-menu/collapsed-menu"; @@ -162,17 +159,21 @@ export default function ThemeEditor() { let soundUrl: string; if (theme.hasCustomSound) { - const themeSoundUrl = await window.electron.getThemeSoundDataUrl(theme.id); + const themeSoundUrl = await window.electron.getThemeSoundDataUrl( + theme.id + ); if (themeSoundUrl) { soundUrl = themeSoundUrl; } else { - const defaultSound = (await import("@renderer/assets/audio/achievement.wav")) - .default; + const defaultSound = ( + await import("@renderer/assets/audio/achievement.wav") + ).default; soundUrl = defaultSound; } } else { - const defaultSound = (await import("@renderer/assets/audio/achievement.wav")) - .default; + const defaultSound = ( + await import("@renderer/assets/audio/achievement.wav") + ).default; soundUrl = defaultSound; }