mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-03 00:33:49 +03:00
feat: add theme page
This commit is contained in:
parent
52f7647c79
commit
148e577f0a
@ -296,7 +296,8 @@
|
||||
"become_subscriber": "Be Hydra Cloud",
|
||||
"subscription_renew_cancelled": "Automatic renewal is disabled",
|
||||
"subscription_renews_on": "Your subscription renews on {{date}}",
|
||||
"bill_sent_until": "Your next bill will be sent until this day"
|
||||
"bill_sent_until": "Your next bill will be sent until this day",
|
||||
"no_themes": "Seems like you don't have any themes yet, but no worries, click here to create your first masterpiece."
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download complete",
|
||||
|
@ -0,0 +1,25 @@
|
||||
@use "../../../../scss/globals.scss";
|
||||
|
||||
.settings-appearance {
|
||||
&__actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
&-left {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&-right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&__button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
import { GlobeIcon, TrashIcon } from "@primer/octicons-react";
|
||||
import { PlusIcon } from "@primer/octicons-react";
|
||||
import { Button } from "@renderer/components/button/button";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import "./theme-actions.scss";
|
||||
|
||||
export const ThemeActions = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="settings-appearance__actions">
|
||||
<div className="settings-appearance__actions-left">
|
||||
<Button theme="primary" className="settings-appearance__button">
|
||||
<GlobeIcon />
|
||||
{t("web_store")}
|
||||
</Button>
|
||||
|
||||
<Button theme="danger" className="settings-appearance__button">
|
||||
<TrashIcon />
|
||||
{t("clear_themes")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="settings-appearance__actions-right">
|
||||
<Button theme="outline" className="settings-appearance__button">
|
||||
<PlusIcon />
|
||||
{t("add_theme")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,93 @@
|
||||
@use "../../../../scss/globals.scss";
|
||||
|
||||
.theme-card {
|
||||
width: 100%;
|
||||
min-height: 160px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: rgba(globals.$border-color, 0.01);
|
||||
border: 1px solid globals.$border-color;
|
||||
border-radius: 12px;
|
||||
gap: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
|
||||
&--active {
|
||||
background-color: rgba(globals.$border-color, 0.04);
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
|
||||
&__title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: globals.$muted-color;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
&__colors {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
|
||||
&__color {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid globals.$border-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__author {
|
||||
font-size: 12px;
|
||||
color: globals.$body-color;
|
||||
font-weight: 400;
|
||||
|
||||
&__name {
|
||||
font-weight: 600;
|
||||
color: rgba(globals.$muted-color, 0.8);
|
||||
margin-left: 4px;
|
||||
|
||||
&:hover {
|
||||
color: globals.$muted-color;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
|
||||
&__left {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
|
||||
Button {
|
||||
padding: 8px 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
import { PencilIcon, TrashIcon } from "@primer/octicons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@renderer/components/button/button";
|
||||
import type { Theme } from "../themes-manager";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import "./theme-card.scss";
|
||||
|
||||
interface ThemeCardProps {
|
||||
theme: Theme;
|
||||
handleSetTheme: (themeId: string) => void;
|
||||
handleDeleteTheme: (themeId: string) => void;
|
||||
}
|
||||
|
||||
export const ThemeCard = ({
|
||||
theme,
|
||||
handleSetTheme,
|
||||
handleDeleteTheme,
|
||||
}: ThemeCardProps) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`theme-card ${theme.isActive ? "theme-card--active" : ""}`}
|
||||
key={theme.name}
|
||||
>
|
||||
<div className="theme-card__header">
|
||||
<div className="theme-card__header__title">{theme.name}</div>
|
||||
|
||||
<div className="theme-card__header__colors">
|
||||
{Object.entries(theme.colors).map(([key, color]) => (
|
||||
<div
|
||||
title={color}
|
||||
style={{ backgroundColor: color }}
|
||||
className="theme-card__header__colors__color"
|
||||
key={key}
|
||||
>
|
||||
{/* color circle */}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{theme.author && theme.authorId && (
|
||||
<p className="theme-card__author">
|
||||
{t("by")}
|
||||
|
||||
<span
|
||||
className="theme-card__author__name"
|
||||
onClick={() => navigate(`/profile/${theme.authorId}`)}
|
||||
>
|
||||
{theme.author}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="theme-card__actions">
|
||||
<div className="theme-card__actions__left">
|
||||
{theme.isActive ? (
|
||||
<Button theme="dark">{t("unset_theme ")}</Button>
|
||||
) : (
|
||||
<Button onClick={() => handleSetTheme(theme.id)} theme="outline">
|
||||
{t("set_theme")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="theme-card__actions__right">
|
||||
<Button title={t("edit_theme")} theme="outline">
|
||||
<PencilIcon />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => handleDeleteTheme(theme.id)}
|
||||
title={t("delete_theme")}
|
||||
theme="outline"
|
||||
>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,39 @@
|
||||
@use "../../../../scss/globals.scss";
|
||||
|
||||
.theme-placeholder {
|
||||
width: 100%;
|
||||
min-height: 160px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 40px 24px;
|
||||
background-color: rgba(globals.$border-color, 0.01);
|
||||
cursor: pointer;
|
||||
border: 1px dashed globals.$border-color;
|
||||
border-radius: 12px;
|
||||
gap: 12px;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(globals.$border-color, 0.03);
|
||||
}
|
||||
|
||||
&__icon {
|
||||
svg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: globals.$body-color;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
&__text {
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
font-size: 14.5px;
|
||||
line-height: 1.6;
|
||||
font-weight: 400;
|
||||
color: rgba(globals.$body-color, 0.85);
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { AlertIcon } from "@primer/octicons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import "./theme-placeholder.scss";
|
||||
|
||||
interface ThemePlaceholderProps {
|
||||
setAddThemeModalVisible: (visible: boolean) => void;
|
||||
}
|
||||
|
||||
export const ThemePlaceholder = ({
|
||||
setAddThemeModalVisible,
|
||||
}: ThemePlaceholderProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<button
|
||||
className="theme-placeholder"
|
||||
onClick={() => setAddThemeModalVisible(true)}
|
||||
>
|
||||
<div className="theme-placeholder__icon">
|
||||
<AlertIcon />
|
||||
</div>
|
||||
|
||||
<p className="theme-placeholder__text">{t("no_themes")}</p>
|
||||
</button>
|
||||
);
|
||||
};
|
8
src/renderer/src/pages/settings/aparence/index.ts
Normal file
8
src/renderer/src/pages/settings/aparence/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export { SettingsAppearance } from "./settings-appearance";
|
||||
export { AddThemeModal } from "./modals/add-theme-modal";
|
||||
export { DeleteAllThemesModal } from "./modals/delete-all-themes-modal";
|
||||
export { DeleteThemeModal } from "./modals/delete-theme-modal";
|
||||
export { ThemeCard } from "./components/theme-card";
|
||||
export { ThemePlaceholder } from "./components/theme-placeholder";
|
||||
export { ThemesManager } from "./themes-manager";
|
||||
export { ThemeActions } from "./components/theme-actions";
|
@ -0,0 +1,40 @@
|
||||
import { Modal } from "@renderer/components/modal/modal";
|
||||
import { TextField } from "@renderer/components/text-field/text-field";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import "./modals.scss";
|
||||
import { Button } from "@renderer/components/button/button";
|
||||
import { useState } from "react";
|
||||
|
||||
interface AddThemeModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const AddThemeModal = ({ visible, onClose }: AddThemeModalProps) => {
|
||||
const { t } = useTranslation("settings");
|
||||
|
||||
const [themeName, setThemeName] = useState("");
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={t("add_theme")}
|
||||
description={t("add_theme_description")}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="add-theme-modal__container">
|
||||
<TextField
|
||||
label={t("theme_name")}
|
||||
placeholder={t("insert_theme_name")}
|
||||
hint={t("theme_name_hint")}
|
||||
value={themeName}
|
||||
onChange={(e) => setThemeName(e.target.value)}
|
||||
/>
|
||||
|
||||
<Button theme="primary" onClick={onClose}>
|
||||
{t("add_theme")}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@ -0,0 +1,35 @@
|
||||
import { Button } from "@renderer/components/button/button";
|
||||
import { Modal } from "@renderer/components/modal/modal";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import "./modals.scss";
|
||||
|
||||
interface DeleteAllThemesModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const DeleteAllThemesModal = ({
|
||||
visible,
|
||||
onClose,
|
||||
}: DeleteAllThemesModalProps) => {
|
||||
const { t } = useTranslation("settings");
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={t("delete_all_themes")}
|
||||
description={t("delete_all_themes_description")}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="delete-all-themes-modal__container">
|
||||
<Button theme="outline" onClick={onClose}>
|
||||
{t("delete_all_themes")}
|
||||
</Button>
|
||||
|
||||
<Button theme="primary" onClick={onClose}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@ -0,0 +1,36 @@
|
||||
import { Button } from "@renderer/components/button/button";
|
||||
import { Modal } from "@renderer/components/modal/modal";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import "./modals.scss";
|
||||
|
||||
interface DeleteThemeModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
themeId: string;
|
||||
}
|
||||
|
||||
export const DeleteThemeModal = ({
|
||||
visible,
|
||||
onClose,
|
||||
}: DeleteThemeModalProps) => {
|
||||
const { t } = useTranslation("settings");
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={t("delete_theme")}
|
||||
description={t("delete_theme_description")}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="delete-all-themes-modal__container">
|
||||
<Button theme="outline" onClick={onClose}>
|
||||
{t("delete_theme")}
|
||||
</Button>
|
||||
|
||||
<Button theme="primary" onClick={onClose}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
15
src/renderer/src/pages/settings/aparence/modals/modals.scss
Normal file
15
src/renderer/src/pages/settings/aparence/modals/modals.scss
Normal file
@ -0,0 +1,15 @@
|
||||
.add-theme-modal {
|
||||
&__container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.delete-all-themes-modal__container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
@ -0,0 +1,154 @@
|
||||
@use "../../../scss/globals.scss";
|
||||
|
||||
.settings-appearance {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
&-left {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&__themes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
&__theme {
|
||||
width: 100%;
|
||||
min-height: 160px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: rgba(globals.$border-color, 0.01);
|
||||
border: 1px solid globals.$border-color;
|
||||
border-radius: 12px;
|
||||
gap: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
|
||||
&--active {
|
||||
background-color: rgba(globals.$border-color, 0.04);
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
|
||||
&__title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: globals.$muted-color;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
&__colors {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
|
||||
&__color {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid globals.$border-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__author {
|
||||
font-size: 12px;
|
||||
color: globals.$body-color;
|
||||
font-weight: 400;
|
||||
|
||||
&__name {
|
||||
font-weight: 600;
|
||||
color: rgba(globals.$muted-color, 0.8);
|
||||
margin-left: 4px;
|
||||
|
||||
&:hover {
|
||||
color: globals.$muted-color;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
|
||||
&__left {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
|
||||
Button {
|
||||
padding: 8px 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__no-themes {
|
||||
width: 100%;
|
||||
min-height: 160px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 40px 24px;
|
||||
background-color: rgba(globals.$border-color, 0.01);
|
||||
cursor: pointer;
|
||||
border: 1px dashed globals.$border-color;
|
||||
border-radius: 12px;
|
||||
gap: 12px;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(globals.$border-color, 0.03);
|
||||
}
|
||||
|
||||
&__icon {
|
||||
svg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: globals.$body-color;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
&__text {
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
font-size: 14.5px;
|
||||
line-height: 1.6;
|
||||
font-weight: 400;
|
||||
color: rgba(globals.$body-color, 0.85);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import "./settings-appearance.scss";
|
||||
import { ThemeActions } from "./index";
|
||||
|
||||
export const SettingsAppearance = () => {
|
||||
return (
|
||||
<div className="settings-appearance">
|
||||
<p className="settings-appearance__description">Appearance</p>
|
||||
|
||||
<ThemeActions />
|
||||
</div>
|
||||
);
|
||||
};
|
19
src/renderer/src/pages/settings/aparence/themes-manager.ts
Normal file
19
src/renderer/src/pages/settings/aparence/themes-manager.ts
Normal file
@ -0,0 +1,19 @@
|
||||
export interface Theme {
|
||||
id: string;
|
||||
name: string;
|
||||
isActive: boolean;
|
||||
description: string;
|
||||
author: string | null;
|
||||
authorId: string | null;
|
||||
version: string;
|
||||
code: string;
|
||||
colors: {
|
||||
accent: string;
|
||||
surface: string;
|
||||
background: string;
|
||||
optional1?: string;
|
||||
optional2?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class ThemesManager {}
|
@ -12,6 +12,7 @@ import { SettingsAccount } from "./settings-account";
|
||||
import { useUserDetails } from "@renderer/hooks";
|
||||
import { useMemo } from "react";
|
||||
import "./settings.scss";
|
||||
import { SettingsAppearance } from "./aparence/settings-appearance";
|
||||
|
||||
export default function Settings() {
|
||||
const { t } = useTranslation("settings");
|
||||
@ -22,6 +23,7 @@ export default function Settings() {
|
||||
t("general"),
|
||||
t("behavior"),
|
||||
t("download_sources"),
|
||||
t("appearance"),
|
||||
"Real-Debrid",
|
||||
];
|
||||
|
||||
@ -47,6 +49,10 @@ export default function Settings() {
|
||||
}
|
||||
|
||||
if (currentCategoryIndex === 3) {
|
||||
return <SettingsAppearance />;
|
||||
}
|
||||
|
||||
if (currentCategoryIndex === 4) {
|
||||
return <SettingsRealDebrid />;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user