mirror of
https://git.srhtnin.garden/sarah/SaraView.git
synced 2026-10-14 10:38:11 +02:00
Web settings in sections
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
0450fee42b
commit
e2cb25e2e1
2 files changed
+509
-376
No files matched your search
@@ -74,6 +74,12 @@ export default function SettingsPanel({
|
||||
const [ExpandedGroup, SetExpandedGroup] = useState(null);
|
||||
const [ConfirmingDeleteGroupName, SetConfirmingDeleteGroupName] = useState(null);
|
||||
|
||||
// Sections down the left, like the other SaraSuite programs' settings.
|
||||
const [Category, SetCategory] = useState("General");
|
||||
const Categories = ["General", "Editor", "Appearance", "Graph", "Profile", "Account"].concat(
|
||||
CurrentUser && CurrentUser.IsAdmin ? ["Users", "Groups", "Locked directories"] : [],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
FetchSettings().then(SetSettings);
|
||||
FetchThemeNames().then(SetThemeNames);
|
||||
@@ -316,402 +322,458 @@ export default function SettingsPanel({
|
||||
</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 directory note files (.sv_*)</label>
|
||||
<Switch Checked={Settings.ShowSvFiles} OnChange={(Event) => UpdateField("ShowSvFiles", 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">Show avatars instead of names in blame</label>
|
||||
<Switch Checked={Settings.ShowBlameAvatars} OnChange={(Event) => UpdateField("ShowBlameAvatars", Event.target.checked)} />
|
||||
</div>
|
||||
|
||||
<div className="SettingsRowInline">
|
||||
<label className="SettingsLabel">Show my edits in others' blame view</label>
|
||||
<Switch Checked={Settings.VisibleInBlame} OnChange={(Event) => UpdateField("VisibleInBlame", 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="SettingsRow">
|
||||
<label className="SettingsLabel">Markdown editing</label>
|
||||
<select
|
||||
value={Settings.MarkdownEditing}
|
||||
onChange={(Event) => UpdateField("MarkdownEditing", Event.target.value)}
|
||||
>
|
||||
<option value="Live">Live: edit the Markdown where you click in the rendered file</option>
|
||||
<option value="Separate">Separate: View and Edit are two modes</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="SettingsSection">
|
||||
<div className="SettingsSectionTitle">Graph</div>
|
||||
<div className="SettingsRowInline">
|
||||
<label className="SettingsLabel">Show directories (off: only files and the links between them)</label>
|
||||
<Switch
|
||||
Checked={Settings.GraphShowDirectories}
|
||||
OnChange={(Event) => UpdateField("GraphShowDirectories", Event.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
<div className="SettingsRowInline">
|
||||
<label className="SettingsLabel">Colour directories by depth (off: all purple)</label>
|
||||
<Switch
|
||||
Checked={Settings.GraphColourByDepth}
|
||||
OnChange={(Event) => UpdateField("GraphColourByDepth", Event.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
<div className="SettingsRowInline">
|
||||
<label className="SettingsLabel">Smaller directory dots the deeper they are</label>
|
||||
<Switch
|
||||
Checked={Settings.GraphShrinkByDepth}
|
||||
OnChange={(Event) => UpdateField("GraphShrinkByDepth", Event.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
<div className="SettingsRow">
|
||||
<label className="SettingsLabel">Pan with mouse button</label>
|
||||
<select
|
||||
value={Settings.GraphPanButton === "Left" ? "Left" : "Right"}
|
||||
onChange={(Event) => UpdateField("GraphPanButton", Event.target.value)}
|
||||
<div className="SettingsBody SettingsSplit">
|
||||
<div className="SettingsCategories">
|
||||
{Categories.map((Name) => (
|
||||
<button
|
||||
key={Name}
|
||||
className={`SettingsCategory${Name === Category ? " SettingsCategoryActive" : ""}`}
|
||||
onClick={() => SetCategory(Name)}
|
||||
>
|
||||
<option value="Right">Right (left-click opens a dot)</option>
|
||||
<option value="Left">Left (right-click opens a dot)</option>
|
||||
</select>
|
||||
</div>
|
||||
{Name}
|
||||
</button>
|
||||
))}
|
||||
</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={HandleRenameThemeClick}>Rename</button>
|
||||
<button onClick={HandleDeleteThemeClick}>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!IsCreatingTheme && (
|
||||
<button onClick={HandleStartCreateTheme}>Create from current theme</button>
|
||||
)}
|
||||
{IsCreatingTheme && NewThemePalette && (
|
||||
<div className="SettingsDetail">
|
||||
{Category === "General" && (
|
||||
<>
|
||||
<div className="SettingsRow">
|
||||
<label className="SettingsLabel">New theme name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={NewThemeName}
|
||||
onChange={(Event) => SetNewThemeName(Event.target.value)}
|
||||
placeholder="MyTheme"
|
||||
/>
|
||||
<div className="SettingsRowInline">
|
||||
<label className="SettingsLabel">Show hidden directories</label>
|
||||
<Switch Checked={Settings.ShowHiddenFolders} OnChange={(Event) => UpdateField("ShowHiddenFolders", Event.target.checked)} />
|
||||
</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 className="SettingsRowInline">
|
||||
<label className="SettingsLabel">Show directory note files (.sv_*)</label>
|
||||
<Switch Checked={Settings.ShowSvFiles} OnChange={(Event) => UpdateField("ShowSvFiles", Event.target.checked)} />
|
||||
</div>
|
||||
<button onClick={HandleSaveNewTheme}>Save theme</button>
|
||||
|
||||
|
||||
<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">Confirm before delete</label>
|
||||
<Switch Checked={Settings.ConfirmBeforeDelete} OnChange={(Event) => UpdateField("ConfirmBeforeDelete", Event.target.checked)} />
|
||||
</div>
|
||||
|
||||
</>
|
||||
)}
|
||||
{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 className="SettingsRow">
|
||||
<label className="SettingsLabel">Username</label>
|
||||
<div className="SettingsRowInline">
|
||||
<input
|
||||
type="text"
|
||||
value={UsernameField}
|
||||
onChange={(Event) => SetUsernameField(Event.target.value)}
|
||||
/>
|
||||
<button onClick={HandleChangeUsername}>Save</button>
|
||||
</div>
|
||||
{UsernameError && <div className="SettingsSaveError">{UsernameError}</div>}
|
||||
<div className="SettingsHint">
|
||||
Renaming logs out any other devices/browsers you're signed in on - this one
|
||||
stays signed in under the new name.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="SettingsRow">
|
||||
<label className="SettingsLabel">Change password</label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Current password"
|
||||
value={CurrentPasswordField}
|
||||
onChange={(Event) => SetCurrentPasswordField(Event.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="New password"
|
||||
value={NewPasswordField}
|
||||
onChange={(Event) => SetNewPasswordField(Event.target.value)}
|
||||
/>
|
||||
<button onClick={HandleChangePassword}>Update password</button>
|
||||
{PasswordChanged && <div className="SettingsHint">Password updated.</div>}
|
||||
{PasswordError && <div className="SettingsSaveError">{PasswordError}</div>}
|
||||
</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)}
|
||||
/>
|
||||
))}
|
||||
{Category === "Editor" && (
|
||||
<>
|
||||
<div className="SettingsRow">
|
||||
<label className="SettingsLabel">Markdown editing</label>
|
||||
<select
|
||||
value={Settings.MarkdownEditing}
|
||||
onChange={(Event) => UpdateField("MarkdownEditing", Event.target.value)}
|
||||
>
|
||||
<option value="Live">Live: edit the Markdown where you click in the rendered file</option>
|
||||
<option value="Separate">Separate: View and Edit are two modes</option>
|
||||
</select>
|
||||
</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>
|
||||
)}
|
||||
|
||||
{CurrentUser && CurrentUser.IsAdmin && (
|
||||
<div className="SettingsSection">
|
||||
<div className="SettingsSectionTitle">Locked Directories (admin)</div>
|
||||
{Locks && Locks.length === 0 && (
|
||||
<div className="SettingsHint">
|
||||
Nothing locked yet - use the 🔒 button next to any directory to lock it.
|
||||
|
||||
<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>
|
||||
)}
|
||||
{Locks &&
|
||||
Locks.map((Lock, Index) => (
|
||||
<div className="SettingsRowInline" key={`${Lock.Root}:${Lock.Path}:${Index}`}>
|
||||
<span className="SettingsLabel">
|
||||
{Lock.Root}/{Lock.Path} -{" "}
|
||||
{[...(Lock.Users || []), ...(Lock.Groups || []).map((G) => `@${G}`)].join(
|
||||
", "
|
||||
)}
|
||||
</span>
|
||||
<button onClick={() => HandleUnlock(Lock)}>Unlock</button>
|
||||
</div>
|
||||
))}
|
||||
{LockError && <div className="SettingsSaveError">{LockError}</div>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{CurrentUser && CurrentUser.IsAdmin && (
|
||||
<div className="SettingsSection">
|
||||
<div className="SettingsSectionTitle">Groups (admin)</div>
|
||||
{Groups &&
|
||||
Groups.map((Group) => (
|
||||
<div key={Group.Name} className="GroupRow">
|
||||
|
||||
<div className="SettingsRowInline">
|
||||
<label className="SettingsLabel">Show avatars instead of names in blame</label>
|
||||
<Switch Checked={Settings.ShowBlameAvatars} OnChange={(Event) => UpdateField("ShowBlameAvatars", Event.target.checked)} />
|
||||
</div>
|
||||
|
||||
|
||||
<div className="SettingsRowInline">
|
||||
<label className="SettingsLabel">Show my edits in others' blame view</label>
|
||||
<Switch Checked={Settings.VisibleInBlame} OnChange={(Event) => UpdateField("VisibleInBlame", 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>
|
||||
|
||||
</>
|
||||
)}
|
||||
{Category === "Appearance" && (
|
||||
<>
|
||||
<div className="SettingsSection">
|
||||
<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">
|
||||
<span className="SettingsLabel">
|
||||
{Group.Name} ({(Group.Members || []).length})
|
||||
</span>
|
||||
<div className="SettingsRowInline">
|
||||
<button
|
||||
onClick={() =>
|
||||
SetExpandedGroup(ExpandedGroup === Group.Name ? null : Group.Name)
|
||||
}
|
||||
>
|
||||
{ExpandedGroup === Group.Name ? "Hide" : "Members"}
|
||||
</button>
|
||||
{Group.Name !== "Admin" && Group.Name !== "Guest" && (
|
||||
<button onClick={() => HandleDeleteGroupClick(Group.Name)}>Delete</button>
|
||||
)}
|
||||
</div>
|
||||
<button onClick={HandleRenameThemeClick}>Rename</button>
|
||||
<button onClick={HandleDeleteThemeClick}>Delete</button>
|
||||
</div>
|
||||
{ExpandedGroup === Group.Name && (
|
||||
<div className="AccessPickerList GroupMemberList">
|
||||
{(Users || []).map((User) => (
|
||||
<label className="AccessPickerRow" key={User.Username}>
|
||||
</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="checkbox"
|
||||
checked={(Group.Members || []).includes(User.Username)}
|
||||
onChange={() => HandleToggleGroupMember(Group, User.Username)}
|
||||
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="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>
|
||||
|
||||
</>
|
||||
)}
|
||||
{Category === "Graph" && (
|
||||
<>
|
||||
<div className="SettingsSection">
|
||||
<div className="SettingsRowInline">
|
||||
<label className="SettingsLabel">Show directories (off: only files and the links between them)</label>
|
||||
<Switch
|
||||
Checked={Settings.GraphShowDirectories}
|
||||
OnChange={(Event) => UpdateField("GraphShowDirectories", Event.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
<div className="SettingsRowInline">
|
||||
<label className="SettingsLabel">Colour directories by depth (off: all purple)</label>
|
||||
<Switch
|
||||
Checked={Settings.GraphColourByDepth}
|
||||
OnChange={(Event) => UpdateField("GraphColourByDepth", Event.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
<div className="SettingsRowInline">
|
||||
<label className="SettingsLabel">Smaller directory dots the deeper they are</label>
|
||||
<Switch
|
||||
Checked={Settings.GraphShrinkByDepth}
|
||||
OnChange={(Event) => UpdateField("GraphShrinkByDepth", Event.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
<div className="SettingsRow">
|
||||
<label className="SettingsLabel">Pan with mouse button</label>
|
||||
<select
|
||||
value={Settings.GraphPanButton === "Left" ? "Left" : "Right"}
|
||||
onChange={(Event) => UpdateField("GraphPanButton", Event.target.value)}
|
||||
>
|
||||
<option value="Right">Right (left-click opens a dot)</option>
|
||||
<option value="Left">Left (right-click opens a dot)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</>
|
||||
)}
|
||||
{Category === "Profile" && (
|
||||
<>
|
||||
<div className="SettingsSection">
|
||||
<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>
|
||||
|
||||
</>
|
||||
)}
|
||||
{Category === "Account" && (
|
||||
<>
|
||||
<div className="SettingsSection">
|
||||
<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 className="SettingsRow">
|
||||
<label className="SettingsLabel">Username</label>
|
||||
<div className="SettingsRowInline">
|
||||
<input
|
||||
type="text"
|
||||
value={UsernameField}
|
||||
onChange={(Event) => SetUsernameField(Event.target.value)}
|
||||
/>
|
||||
<button onClick={HandleChangeUsername}>Save</button>
|
||||
</div>
|
||||
{UsernameError && <div className="SettingsSaveError">{UsernameError}</div>}
|
||||
<div className="SettingsHint">
|
||||
Renaming logs out any other devices/browsers you're signed in on - this one
|
||||
stays signed in under the new name.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="SettingsRow">
|
||||
<label className="SettingsLabel">Change password</label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Current password"
|
||||
value={CurrentPasswordField}
|
||||
onChange={(Event) => SetCurrentPasswordField(Event.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="New password"
|
||||
value={NewPasswordField}
|
||||
onChange={(Event) => SetNewPasswordField(Event.target.value)}
|
||||
/>
|
||||
<button onClick={HandleChangePassword}>Update password</button>
|
||||
{PasswordChanged && <div className="SettingsHint">Password updated.</div>}
|
||||
{PasswordError && <div className="SettingsSaveError">{PasswordError}</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</>
|
||||
)}
|
||||
{Category === "Users" && (
|
||||
<>
|
||||
{CurrentUser && CurrentUser.IsAdmin && (
|
||||
<div className="SettingsSection">
|
||||
{Users &&
|
||||
Users.map((User) => (
|
||||
<div className="SettingsRowInline" key={User.Username}>
|
||||
<span className="SettingsLabel">
|
||||
<span
|
||||
className="ColourSwatchSmall"
|
||||
style={{ background: `var(--${User.Colour || "Purple"})` }}
|
||||
/>
|
||||
<span>{User.Username}</span>
|
||||
</label>
|
||||
{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>
|
||||
))}
|
||||
<div className="SettingsRowInline">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="New group name"
|
||||
value={NewGroupName}
|
||||
onChange={(Event) => SetNewGroupName(Event.target.value)}
|
||||
/>
|
||||
<button onClick={HandleCreateGroup}>Create group</button>
|
||||
</div>
|
||||
{GroupError && <div className="SettingsSaveError">{GroupError}</div>}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{SaveError && <div className="SettingsSaveError">{SaveError}</div>}
|
||||
</>
|
||||
)}
|
||||
{Category === "Groups" && (
|
||||
<>
|
||||
{CurrentUser && CurrentUser.IsAdmin && (
|
||||
<div className="SettingsSection">
|
||||
{Groups &&
|
||||
Groups.map((Group) => (
|
||||
<div key={Group.Name} className="GroupRow">
|
||||
<div className="SettingsRowInline">
|
||||
<span className="SettingsLabel">
|
||||
{Group.Name} ({(Group.Members || []).length})
|
||||
</span>
|
||||
<div className="SettingsRowInline">
|
||||
<button
|
||||
onClick={() =>
|
||||
SetExpandedGroup(ExpandedGroup === Group.Name ? null : Group.Name)
|
||||
}
|
||||
>
|
||||
{ExpandedGroup === Group.Name ? "Hide" : "Members"}
|
||||
</button>
|
||||
{Group.Name !== "Admin" && Group.Name !== "Guest" && (
|
||||
<button onClick={() => HandleDeleteGroupClick(Group.Name)}>Delete</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{ExpandedGroup === Group.Name && (
|
||||
<div className="AccessPickerList GroupMemberList">
|
||||
{(Users || []).map((User) => (
|
||||
<label className="AccessPickerRow" key={User.Username}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={(Group.Members || []).includes(User.Username)}
|
||||
onChange={() => HandleToggleGroupMember(Group, User.Username)}
|
||||
/>
|
||||
<span
|
||||
className="ColourSwatchSmall"
|
||||
style={{ background: `var(--${User.Colour || "Purple"})` }}
|
||||
/>
|
||||
<span>{User.Username}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="SettingsRowInline">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="New group name"
|
||||
value={NewGroupName}
|
||||
onChange={(Event) => SetNewGroupName(Event.target.value)}
|
||||
/>
|
||||
<button onClick={HandleCreateGroup}>Create group</button>
|
||||
</div>
|
||||
{GroupError && <div className="SettingsSaveError">{GroupError}</div>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</>
|
||||
)}
|
||||
{Category === "Locked directories" && (
|
||||
<>
|
||||
{CurrentUser && CurrentUser.IsAdmin && (
|
||||
<div className="SettingsSection">
|
||||
{Locks && Locks.length === 0 && (
|
||||
<div className="SettingsHint">
|
||||
Nothing locked yet - use the 🔒 button next to any directory to lock it.
|
||||
</div>
|
||||
)}
|
||||
{Locks &&
|
||||
Locks.map((Lock, Index) => (
|
||||
<div className="SettingsRowInline" key={`${Lock.Root}:${Lock.Path}:${Index}`}>
|
||||
<span className="SettingsLabel">
|
||||
{Lock.Root}/{Lock.Path} -{" "}
|
||||
{[...(Lock.Users || []), ...(Lock.Groups || []).map((G) => `@${G}`)].join(
|
||||
", "
|
||||
)}
|
||||
</span>
|
||||
<button onClick={() => HandleUnlock(Lock)}>Unlock</button>
|
||||
</div>
|
||||
))}
|
||||
{LockError && <div className="SettingsSaveError">{LockError}</div>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</>
|
||||
)}
|
||||
|
||||
{SaveError && <div className="SettingsSaveError">{SaveError}</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="SettingsFooter">
|
||||
|
||||
@@ -983,9 +983,10 @@ img,
|
||||
}
|
||||
|
||||
.SettingsModal {
|
||||
width: 420px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
width: min(700px, calc(100vw - 32px));
|
||||
height: min(560px, 85vh);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--Surface);
|
||||
border: 1px solid var(--Purple);
|
||||
border-radius: 8px;
|
||||
@@ -1020,6 +1021,76 @@ img,
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* Sections down the left, the open one on the right (like the other
|
||||
SaraSuite programs' settings); stacked on narrow screens. */
|
||||
.SettingsSplit {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
flex-direction: row;
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.SettingsCategories {
|
||||
flex: 0 0 150px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 8px;
|
||||
background: var(--SurfaceAlt);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.SettingsCategories .SettingsCategory {
|
||||
text-align: left;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 6px 10px;
|
||||
color: var(--Text);
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.SettingsCategories .SettingsCategory:hover {
|
||||
background: var(--Surface);
|
||||
}
|
||||
|
||||
/* Beats .SettingsBody button. */
|
||||
.SettingsCategories .SettingsCategoryActive,
|
||||
.SettingsCategories .SettingsCategoryActive:hover {
|
||||
background: var(--Border);
|
||||
color: var(--Purple);
|
||||
}
|
||||
|
||||
.SettingsDetail {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.SettingsDetail > .SettingsSection:first-child {
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.SettingsSplit {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.SettingsCategories {
|
||||
flex: 0 0 auto;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.SettingsRow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in new issue
Block a user