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