mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-18 16:53:57 +00:00
feat: adding slider to achievement sound
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Автоматически начинать воспроизведение трейлеров на странице игры",
|
||||
|
||||
@@ -32,6 +32,7 @@ const copyThemeAchievementSound = async (
|
||||
await themesSublevel.put(themeId, {
|
||||
...theme,
|
||||
hasCustomSound: true,
|
||||
originalSoundPath: sourcePath,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -31,6 +31,7 @@ const removeThemeAchievementSound = async (
|
||||
await themesSublevel.put(themeId, {
|
||||
...theme,
|
||||
hasCustomSound: false,
|
||||
originalSoundPath: undefined,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface Theme {
|
||||
isActive: boolean;
|
||||
code: string;
|
||||
hasCustomSound?: boolean;
|
||||
originalSoundPath?: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user