feat: add theme page

This commit is contained in:
Hachi-R 2025-01-23 15:23:07 -03:00
parent 52f7647c79
commit 148e577f0a
16 changed files with 626 additions and 1 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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";

View File

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

View File

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

View File

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

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

View File

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

View File

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

View 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 {}

View File

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