mirror of
https://git.srhtnin.garden/sarah/SaraView.git
synced 2026-10-14 10:38:11 +02:00
Initial commit
This commit is contained in:
commit
8aa223bc7b
40 files changed
+4320
No files matched your search
@@ -0,0 +1,424 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
CreateUserAccount,
|
||||
DeleteTheme,
|
||||
FetchProfileColourOptions,
|
||||
FetchSettings,
|
||||
FetchTheme,
|
||||
FetchThemeNames,
|
||||
FetchUsers,
|
||||
GetAvatarUrl,
|
||||
Logout,
|
||||
SaveSettings,
|
||||
SaveTheme,
|
||||
UpdateProfileColour,
|
||||
UploadAvatar,
|
||||
} from "../Api.js";
|
||||
import Switch from "./Switch.jsx";
|
||||
import { PaletteRoles } from "../ThemeRoles.js";
|
||||
|
||||
export default function SettingsPanel({
|
||||
CurrentUser,
|
||||
OnClose,
|
||||
OnSettingsChanged,
|
||||
OnLoggedOut,
|
||||
OnProfileUpdated,
|
||||
}) {
|
||||
const [Settings, SetSettings] = useState(null);
|
||||
const [ThemeNames, SetThemeNames] = useState([]);
|
||||
const [SaveError, SetSaveError] = useState(null);
|
||||
const [IsSaving, SetIsSaving] = useState(false);
|
||||
|
||||
const [IsCreatingTheme, SetIsCreatingTheme] = useState(false);
|
||||
const [NewThemeName, SetNewThemeName] = useState("");
|
||||
const [NewThemePalette, SetNewThemePalette] = useState(null);
|
||||
const [ThemeError, SetThemeError] = useState(null);
|
||||
|
||||
const [Users, SetUsers] = useState(null);
|
||||
const [NewUsername, SetNewUsername] = useState("");
|
||||
const [NewUserPassword, SetNewUserPassword] = useState("");
|
||||
const [NewUserIsAdmin, SetNewUserIsAdmin] = useState(false);
|
||||
const [NewUserColour, SetNewUserColour] = useState("Purple");
|
||||
const [UserError, SetUserError] = useState(null);
|
||||
|
||||
const [ColourOptions, SetColourOptions] = useState([]);
|
||||
const [ProfileError, SetProfileError] = useState(null);
|
||||
const [AvatarNonce, SetAvatarNonce] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
FetchSettings().then(SetSettings);
|
||||
FetchThemeNames().then(SetThemeNames);
|
||||
FetchProfileColourOptions().then(SetColourOptions);
|
||||
if (CurrentUser && CurrentUser.IsAdmin) {
|
||||
FetchUsers().then(SetUsers);
|
||||
}
|
||||
}, [CurrentUser]);
|
||||
|
||||
if (!Settings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function UpdateField(Field, Value) {
|
||||
SetSettings({ ...Settings, [Field]: Value });
|
||||
}
|
||||
|
||||
async function HandleSave() {
|
||||
SetIsSaving(true);
|
||||
SetSaveError(null);
|
||||
try {
|
||||
const Saved = await SaveSettings(Settings);
|
||||
OnSettingsChanged(Saved);
|
||||
OnClose();
|
||||
} catch (Error) {
|
||||
SetSaveError(Error.message);
|
||||
} finally {
|
||||
SetIsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function HandleStartCreateTheme() {
|
||||
SetThemeError(null);
|
||||
const BasePalette = await FetchTheme(Settings.ThemeName);
|
||||
SetNewThemePalette(BasePalette);
|
||||
SetNewThemeName("");
|
||||
SetIsCreatingTheme(true);
|
||||
}
|
||||
|
||||
async function HandleSaveNewTheme() {
|
||||
if (!NewThemeName.trim()) return;
|
||||
|
||||
try {
|
||||
await SaveTheme(NewThemeName.trim(), NewThemePalette);
|
||||
const Updated = await FetchThemeNames();
|
||||
SetThemeNames(Updated);
|
||||
UpdateField("ThemeName", NewThemeName.trim());
|
||||
SetIsCreatingTheme(false);
|
||||
} catch (Error) {
|
||||
SetThemeError(Error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function HandleRenameTheme() {
|
||||
const NewName = window.prompt("Rename theme to:", Settings.ThemeName);
|
||||
if (!NewName || !NewName.trim() || NewName.trim() === Settings.ThemeName) return;
|
||||
|
||||
SetThemeError(null);
|
||||
try {
|
||||
const Palette = await FetchTheme(Settings.ThemeName);
|
||||
await SaveTheme(NewName.trim(), Palette);
|
||||
await DeleteTheme(Settings.ThemeName);
|
||||
const Updated = await FetchThemeNames();
|
||||
SetThemeNames(Updated);
|
||||
UpdateField("ThemeName", NewName.trim());
|
||||
} catch (Error) {
|
||||
SetThemeError(Error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function HandleDeleteTheme() {
|
||||
if (ThemeNames.length <= 1) {
|
||||
SetThemeError("Can't delete the only remaining theme.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.confirm(`Delete theme "${Settings.ThemeName}"? This can't be undone.`)) return;
|
||||
|
||||
SetThemeError(null);
|
||||
try {
|
||||
await DeleteTheme(Settings.ThemeName);
|
||||
const Updated = await FetchThemeNames();
|
||||
SetThemeNames(Updated);
|
||||
UpdateField("ThemeName", Updated[0]);
|
||||
} catch (Error) {
|
||||
SetThemeError(Error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function HandleLogout() {
|
||||
await Logout();
|
||||
OnLoggedOut();
|
||||
}
|
||||
|
||||
async function HandleCreateUser() {
|
||||
SetUserError(null);
|
||||
|
||||
try {
|
||||
await CreateUserAccount(NewUsername.trim(), NewUserPassword, NewUserIsAdmin, NewUserColour);
|
||||
const Updated = await FetchUsers();
|
||||
SetUsers(Updated);
|
||||
SetNewUsername("");
|
||||
SetNewUserPassword("");
|
||||
SetNewUserIsAdmin(false);
|
||||
SetNewUserColour("Purple");
|
||||
} catch (Error) {
|
||||
SetUserError(Error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function HandlePickColour(Colour) {
|
||||
SetProfileError(null);
|
||||
try {
|
||||
const Updated = await UpdateProfileColour(Colour);
|
||||
OnProfileUpdated(Updated);
|
||||
} catch (Error) {
|
||||
SetProfileError(Error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function HandleAvatarChange(Event) {
|
||||
const File = Event.target.files && Event.target.files[0];
|
||||
if (!File) return;
|
||||
|
||||
SetProfileError(null);
|
||||
try {
|
||||
await UploadAvatar(File);
|
||||
const NextNonce = Date.now();
|
||||
SetAvatarNonce(NextNonce);
|
||||
OnProfileUpdated({ AvatarNonce: NextNonce });
|
||||
} catch (Error) {
|
||||
SetProfileError(Error.message);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="SettingsOverlay" onClick={OnClose}>
|
||||
<div className="SettingsModal" onClick={(Event) => Event.stopPropagation()}>
|
||||
<div className="SettingsHeader">
|
||||
<span>Settings</span>
|
||||
<button className="SettingsClose" onClick={OnClose}>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="SettingsBody">
|
||||
<div className="SettingsRow">
|
||||
<label className="SettingsLabel">Font family</label>
|
||||
<input
|
||||
type="text"
|
||||
value={Settings.FontFamily}
|
||||
onChange={(Event) => UpdateField("FontFamily", Event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="SettingsRow">
|
||||
<label className="SettingsLabel">Font size (px)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={Settings.FontSize}
|
||||
onChange={(Event) => UpdateField("FontSize", Number(Event.target.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="SettingsRowInline">
|
||||
<label className="SettingsLabel">Show hidden directories</label>
|
||||
<Switch Checked={Settings.ShowHiddenFolders} OnChange={(Event) => UpdateField("ShowHiddenFolders", Event.target.checked)} />
|
||||
</div>
|
||||
|
||||
<div className="SettingsRowInline">
|
||||
<label className="SettingsLabel">Show breadcrumbs</label>
|
||||
<Switch Checked={Settings.ShowBreadcrumbs} OnChange={(Event) => UpdateField("ShowBreadcrumbs", Event.target.checked)} />
|
||||
</div>
|
||||
|
||||
<div className="SettingsRowInline">
|
||||
<label className="SettingsLabel">Hyphen file icons (-)</label>
|
||||
<Switch Checked={Settings.UseHyphenBullets} OnChange={(Event) => UpdateField("UseHyphenBullets", Event.target.checked)} />
|
||||
</div>
|
||||
|
||||
<div className="SettingsRowInline">
|
||||
<label className="SettingsLabel">ASCII directory icons (>/v)</label>
|
||||
<Switch Checked={Settings.UseAsciiFolderIcons} OnChange={(Event) => UpdateField("UseAsciiFolderIcons", Event.target.checked)} />
|
||||
</div>
|
||||
|
||||
<div className="SettingsRowInline">
|
||||
<label className="SettingsLabel">Open directory file on click</label>
|
||||
<Switch Checked={Settings.NavigateIntoDirectories} OnChange={(Event) => UpdateField("NavigateIntoDirectories", Event.target.checked)} />
|
||||
</div>
|
||||
|
||||
<div className="SettingsRowInline">
|
||||
<label className="SettingsLabel">New File/Directory buttons at top of sidebar</label>
|
||||
<Switch Checked={Settings.NewEntryButtonsAtTop} OnChange={(Event) => UpdateField("NewEntryButtonsAtTop", Event.target.checked)} />
|
||||
</div>
|
||||
|
||||
<div className="SettingsRowInline">
|
||||
<label className="SettingsLabel">Show blame gutter (who last changed each line)</label>
|
||||
<Switch Checked={Settings.ShowGitBlame} OnChange={(Event) => UpdateField("ShowGitBlame", Event.target.checked)} />
|
||||
</div>
|
||||
|
||||
<div className="SettingsRowInline">
|
||||
<label className="SettingsLabel">Sticky edit/view/save toolbar</label>
|
||||
<Switch Checked={Settings.StickyToolbar} OnChange={(Event) => UpdateField("StickyToolbar", Event.target.checked)} />
|
||||
</div>
|
||||
|
||||
<div className="SettingsRowInline">
|
||||
<label className="SettingsLabel">Confirm before delete</label>
|
||||
<Switch Checked={Settings.ConfirmBeforeDelete} OnChange={(Event) => UpdateField("ConfirmBeforeDelete", Event.target.checked)} />
|
||||
</div>
|
||||
|
||||
<div className="SettingsSection">
|
||||
<div className="SettingsSectionTitle">Colour theme</div>
|
||||
<div className="SettingsRow">
|
||||
<select
|
||||
value={Settings.ThemeName}
|
||||
onChange={(Event) => UpdateField("ThemeName", Event.target.value)}
|
||||
>
|
||||
{ThemeNames.map((Name) => (
|
||||
<option key={Name} value={Name}>
|
||||
{Name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="SettingsRowInline">
|
||||
<button onClick={HandleRenameTheme}>Rename</button>
|
||||
<button onClick={HandleDeleteTheme}>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!IsCreatingTheme && (
|
||||
<button onClick={HandleStartCreateTheme}>Create from current theme</button>
|
||||
)}
|
||||
{IsCreatingTheme && NewThemePalette && (
|
||||
<>
|
||||
<div className="SettingsRow">
|
||||
<label className="SettingsLabel">New theme name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={NewThemeName}
|
||||
onChange={(Event) => SetNewThemeName(Event.target.value)}
|
||||
placeholder="MyTheme"
|
||||
/>
|
||||
</div>
|
||||
<div className="PaletteGrid">
|
||||
{PaletteRoles.map((Role) => (
|
||||
<div className="PaletteSwatchRow" key={Role}>
|
||||
<input
|
||||
type="color"
|
||||
value={NewThemePalette[Role] || "#000000"}
|
||||
onChange={(Event) =>
|
||||
SetNewThemePalette({ ...NewThemePalette, [Role]: Event.target.value })
|
||||
}
|
||||
/>
|
||||
<span>{Role}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button onClick={HandleSaveNewTheme}>Save theme</button>
|
||||
</>
|
||||
)}
|
||||
{ThemeError && <div className="SettingsSaveError">{ThemeError}</div>}
|
||||
</div>
|
||||
|
||||
<div className="SettingsSection">
|
||||
<div className="SettingsSectionTitle">Profile</div>
|
||||
<div className="SettingsRow">
|
||||
<label className="SettingsLabel">Your colour</label>
|
||||
<div className="ColourSwatchPicker">
|
||||
{ColourOptions.map((Colour) => (
|
||||
<button
|
||||
key={Colour}
|
||||
className={`ColourSwatch${CurrentUser && CurrentUser.Colour === Colour ? " ColourSwatchSelected" : ""}`}
|
||||
style={{ background: `var(--${Colour})` }}
|
||||
title={Colour}
|
||||
onClick={() => HandlePickColour(Colour)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="SettingsRow">
|
||||
<label className="SettingsLabel">Avatar</label>
|
||||
<div className="AvatarUploadRow">
|
||||
{CurrentUser && (
|
||||
<img
|
||||
className="AvatarPreview"
|
||||
src={`${GetAvatarUrl(CurrentUser.Username)}&v=${AvatarNonce}`}
|
||||
alt=""
|
||||
onError={(Event) => {
|
||||
Event.target.style.visibility = "hidden";
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<input type="file" accept="image/*" onChange={HandleAvatarChange} />
|
||||
</div>
|
||||
</div>
|
||||
{ProfileError && <div className="SettingsSaveError">{ProfileError}</div>}
|
||||
</div>
|
||||
|
||||
<div className="SettingsSection">
|
||||
<div className="SettingsSectionTitle">Account</div>
|
||||
<div className="SettingsRowInline">
|
||||
<span className="SettingsLabel">
|
||||
Logged in as {CurrentUser ? CurrentUser.Username : "..."}
|
||||
{CurrentUser && CurrentUser.IsAdmin ? " (admin)" : ""}
|
||||
</span>
|
||||
<button onClick={HandleLogout}>Log out</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{CurrentUser && CurrentUser.IsAdmin && (
|
||||
<div className="SettingsSection">
|
||||
<div className="SettingsSectionTitle">Users (admin)</div>
|
||||
{Users &&
|
||||
Users.map((User) => (
|
||||
<div className="SettingsRowInline" key={User.Username}>
|
||||
<span className="SettingsLabel">
|
||||
<span
|
||||
className="ColourSwatchSmall"
|
||||
style={{ background: `var(--${User.Colour || "Purple"})` }}
|
||||
/>
|
||||
{User.Username}
|
||||
{User.IsAdmin ? " (admin)" : ""}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="SettingsRow">
|
||||
<label className="SettingsLabel">New username</label>
|
||||
<input
|
||||
type="text"
|
||||
value={NewUsername}
|
||||
onChange={(Event) => SetNewUsername(Event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="SettingsRow">
|
||||
<label className="SettingsLabel">New password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={NewUserPassword}
|
||||
onChange={(Event) => SetNewUserPassword(Event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="SettingsRow">
|
||||
<label className="SettingsLabel">Colour</label>
|
||||
<div className="ColourSwatchPicker">
|
||||
{ColourOptions.map((Colour) => (
|
||||
<button
|
||||
key={Colour}
|
||||
className={`ColourSwatch${NewUserColour === Colour ? " ColourSwatchSelected" : ""}`}
|
||||
style={{ background: `var(--${Colour})` }}
|
||||
title={Colour}
|
||||
onClick={() => SetNewUserColour(Colour)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="SettingsRowInline">
|
||||
<label className="SettingsLabel">Admin</label>
|
||||
<Switch Checked={NewUserIsAdmin} OnChange={(Event) => SetNewUserIsAdmin(Event.target.checked)} />
|
||||
</div>
|
||||
{UserError && <div className="SettingsSaveError">{UserError}</div>}
|
||||
<button onClick={HandleCreateUser}>Create user</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{SaveError && <div className="SettingsSaveError">{SaveError}</div>}
|
||||
</div>
|
||||
|
||||
<div className="SettingsFooter">
|
||||
<button onClick={OnClose}>Cancel</button>
|
||||
<button onClick={HandleSave} disabled={IsSaving}>
|
||||
{IsSaving ? "Saving…" : "Save"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user