mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-24 03:11:03 +00:00
feat: adding slider to achievement sound
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user