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

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