feat: adding slider to achievement sound

This commit is contained in:
Chubby Granny Chaser
2025-11-10 22:55:17 +00:00
parent 482d9b2f96
commit 399669a94c
11 changed files with 233 additions and 168 deletions

View File

@@ -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",

View File

@@ -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.",

View File

@@ -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",

View File

@@ -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": "Автоматически начинать воспроизведение трейлеров на странице игры",

View File

@@ -32,6 +32,7 @@ const copyThemeAchievementSound = async (
await themesSublevel.put(themeId, {
...theme,
hasCustomSound: true,
originalSoundPath: sourcePath,
updatedAt: new Date(),
});
};

View File

@@ -31,6 +31,7 @@ const removeThemeAchievementSound = async (
await themesSublevel.put(themeId, {
...theme,
hasCustomSound: false,
originalSoundPath: undefined,
updatedAt: new Date(),
});
};

View File

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

View File

@@ -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() {
<label htmlFor="achievement-volume">
{t("achievement_sound_volume")}
</label>
<div className="settings-general__volume-input-wrapper">
<div className="settings-general__volume-input-container">
<input
id="achievement-volume"
type="number"
min="0"
max="100"
value={form.achievementSoundVolume}
onChange={(e) => {
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);
}
}}
/>
<span className="settings-general__volume-input-unit">%</span>
</div>
<div className="settings-general__volume-input-buttons">
<button
type="button"
onClick={(e) => {
e.preventDefault();
const newVolume = Math.min(
100,
form.achievementSoundVolume + 1
);
handleVolumeChange(newVolume);
}}
>
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="currentColor"
>
<path d="M6 4l4 4H2l4-4z" />
</svg>
</button>
<button
type="button"
onClick={(e) => {
e.preventDefault();
const newVolume = Math.max(
0,
form.achievementSoundVolume - 1
);
handleVolumeChange(newVolume);
}}
>
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="currentColor"
>
<path d="M6 8L2 4h8L6 8z" />
</svg>
</button>
</div>
<div className="settings-general__volume-slider-wrapper">
<UnmuteIcon size={16} className="settings-general__volume-icon" />
<input
id="achievement-volume"
type="range"
min="0"
max="100"
value={form.achievementSoundVolume}
onChange={(e) => {
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
}
/>
<span className="settings-general__volume-value">
{form.achievementSoundVolume}%
</span>
</div>
</div>
)}

View File

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

View File

@@ -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<Theme | null>(null);
const [code, setCode] = useState("");
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [soundPath, setSoundPath] = useState<string>("");
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}
/>
</div>
</div>
<div className="theme-editor__sound-controls">
<TextField
label={t("select_achievement_sound")}
value={soundPath || ""}
placeholder={soundPath ? undefined : t("no_sound_file_selected")}
readOnly
disabled
rightContent={
<Button theme="outline" onClick={handleSelectSound}>
<UploadIcon />
{theme?.hasCustomSound
? t("change_achievement_sound")
: t("select_achievement_sound")}
{t("select")}
</Button>
}
/>
{theme?.hasCustomSound && (
<Button theme="outline" onClick={handleRemoveSound}>
<TrashIcon />
{t("remove_achievement_sound")}
</Button>
)}
{theme?.hasCustomSound && (
<div className="theme-editor__sound-actions-row">
<Button theme="outline" onClick={handleRemoveSound}>
<TrashIcon />
{t("remove")}
</Button>
<Button theme="outline" onClick={handlePreviewSound}>
<PlayIcon />
{t("preview_sound")}
{t("preview")}
</Button>
</div>
</div>
)}
<div className="theme-editor__notification-preview-wrapper">
<root.div>

View File

@@ -6,6 +6,7 @@ export interface Theme {
isActive: boolean;
code: string;
hasCustomSound?: boolean;
originalSoundPath?: string;
createdAt: Date;
updatedAt: Date;
}