mirror of
https://git.srhtnin.garden/sarah/SaraView.git
synced 2026-10-14 02:38:11 +02:00
436 lines
14 KiB
React
436 lines
14 KiB
React
import React, { useEffect, useState } from "react";
|
|
|
|
import Sidebar from "./Components/Sidebar.jsx";
|
|
import Breadcrumb from "./Components/Breadcrumb.jsx";
|
|
import PreviewPane from "./Components/PreviewPane.jsx";
|
|
import DirectoryPane from "./Components/DirectoryPane.jsx";
|
|
import TagPanel from "./Components/TagPanel.jsx";
|
|
import SettingsPanel from "./Components/SettingsPanel.jsx";
|
|
import GraphPane from "./Components/GraphPane.jsx";
|
|
import LoginScreen from "./Components/LoginScreen.jsx";
|
|
import ErrorToast from "./Components/ErrorToast.jsx";
|
|
import {
|
|
EnsureDirectoryNote,
|
|
FetchAuthStatus,
|
|
FetchMe,
|
|
FetchRoots,
|
|
FetchSettings,
|
|
FetchTheme,
|
|
FetchTree,
|
|
} from "./Api.js";
|
|
import { PaletteRoles } from "./ThemeRoles.js";
|
|
|
|
function ApplyTheme(Palette) {
|
|
const RootStyle = document.documentElement.style;
|
|
PaletteRoles.forEach((Role) => {
|
|
if (Palette[Role]) {
|
|
RootStyle.setProperty(`--${Role}`, Palette[Role]);
|
|
}
|
|
});
|
|
}
|
|
|
|
function ApplyFont(Settings) {
|
|
const RootStyle = document.documentElement.style;
|
|
RootStyle.setProperty(
|
|
"--FontFamily",
|
|
`"${Settings.FontFamily}", "Cascadia Mono", monospace`
|
|
);
|
|
RootStyle.setProperty("--FontSize", `${Settings.FontSize}px`);
|
|
}
|
|
|
|
function GetParentPath(FilePath) {
|
|
return FilePath.includes("/") ? FilePath.slice(0, FilePath.lastIndexOf("/")) : "";
|
|
}
|
|
|
|
function BuildLocationPath(RootName, ViewPath, ViewIsDirectory) {
|
|
const Segments = [RootName, ...ViewPath.split("/").filter(Boolean)];
|
|
const Encoded = "/" + Segments.map(encodeURIComponent).join("/");
|
|
return ViewIsDirectory ? `${Encoded}/` : Encoded;
|
|
}
|
|
|
|
function ParseLocationPath(Pathname) {
|
|
const IsDirectory = Pathname.endsWith("/");
|
|
const Segments = Pathname.split("/").filter(Boolean).map(decodeURIComponent);
|
|
const [RootName, ...RestSegments] = Segments;
|
|
return { RootName: RootName || null, Path: RestSegments.join("/"), IsDirectory };
|
|
}
|
|
|
|
const GuestUser = { Username: null, IsAdmin: false, IsGuest: true, Colour: "Purple" };
|
|
|
|
export default function App() {
|
|
const [AuthState, SetAuthState] = useState("Loading");
|
|
const [CurrentUser, SetCurrentUser] = useState(null);
|
|
|
|
const [Roots, SetRoots] = useState([]);
|
|
const [ActiveRoot, SetActiveRoot] = useState(null);
|
|
const [ViewPath, SetViewPath] = useState("");
|
|
const [ViewIsDirectory, SetViewIsDirectory] = useState(true);
|
|
const [ViewFileType, SetViewFileType] = useState(null);
|
|
const [ViewNoteFilePath, SetViewNoteFilePath] = useState(null);
|
|
const [CurrentDirectoryPath, SetCurrentDirectoryPath] = useState("");
|
|
const [SidebarKey, SetSidebarKey] = useState(0);
|
|
const [Settings, SetSettings] = useState(null);
|
|
const [IsSettingsOpen, SetIsSettingsOpen] = useState(false);
|
|
const [IsGraphOpen, SetIsGraphOpen] = useState(false);
|
|
const [ErrorMessage, SetErrorMessage] = useState(null);
|
|
|
|
useEffect(() => {
|
|
FetchAuthStatus().then((Status) => {
|
|
if (!Status.SetupComplete) {
|
|
SetAuthState("Bootstrap");
|
|
return;
|
|
}
|
|
|
|
FetchMe()
|
|
.then((User) => {
|
|
SetCurrentUser(User);
|
|
SetAuthState("LoggedIn");
|
|
})
|
|
.catch(() => {
|
|
SetCurrentUser(GuestUser);
|
|
SetAuthState("Guest");
|
|
});
|
|
});
|
|
}, []);
|
|
|
|
const IsUsable = AuthState === "LoggedIn" || AuthState === "Guest";
|
|
|
|
useEffect(() => {
|
|
if (!IsUsable) return;
|
|
|
|
FetchRoots().then((Fetched) => {
|
|
SetRoots(Fetched);
|
|
RestoreFromLocation(Fetched);
|
|
});
|
|
|
|
FetchSettings().then((FetchedSettings) => {
|
|
SetSettings(FetchedSettings);
|
|
ApplyFont(FetchedSettings);
|
|
FetchTheme(FetchedSettings.ThemeName).then(ApplyTheme);
|
|
});
|
|
}, [AuthState]);
|
|
|
|
useEffect(() => {
|
|
if (!IsUsable || !ActiveRoot) return;
|
|
|
|
const Url = BuildLocationPath(ActiveRoot, ViewPath, ViewIsDirectory);
|
|
if (window.location.pathname !== Url) {
|
|
window.history.pushState({}, "", Url);
|
|
}
|
|
}, [AuthState, ActiveRoot, ViewPath, ViewIsDirectory]);
|
|
|
|
useEffect(() => {
|
|
if (!IsUsable) return;
|
|
|
|
function HandlePopState() {
|
|
RestoreFromLocation(Roots);
|
|
}
|
|
|
|
window.addEventListener("popstate", HandlePopState);
|
|
return () => window.removeEventListener("popstate", HandlePopState);
|
|
}, [AuthState, Roots]);
|
|
|
|
async function RestoreFromLocation(RootsList) {
|
|
const { RootName, Path, IsDirectory } = ParseLocationPath(window.location.pathname);
|
|
const MatchedRoot = RootsList.find((Root) => Root.Name === RootName);
|
|
|
|
if (!MatchedRoot) {
|
|
if (RootsList.length > 0) {
|
|
SetActiveRoot(RootsList[0].Name);
|
|
}
|
|
return;
|
|
}
|
|
|
|
SetActiveRoot(RootName);
|
|
|
|
if (!Path) {
|
|
HandleNavigateDirectory("");
|
|
return;
|
|
}
|
|
|
|
const LastSlash = Path.lastIndexOf("/");
|
|
const ParentPath = LastSlash === -1 ? "" : Path.slice(0, LastSlash);
|
|
const EntryName = LastSlash === -1 ? Path : Path.slice(LastSlash + 1);
|
|
|
|
try {
|
|
const Entries = await FetchTree(RootName, ParentPath);
|
|
const Entry = Entries.find((Candidate) => Candidate.Name === EntryName);
|
|
|
|
if (!Entry) {
|
|
HandleNavigateDirectory("");
|
|
return;
|
|
}
|
|
|
|
if (Entry.IsDirectory) {
|
|
if (IsDirectory) {
|
|
HandleNavigateDirectory(Path);
|
|
} else {
|
|
OpenDirectoryNote(Path, Entry.Note);
|
|
}
|
|
} else {
|
|
HandleSelectFile(Path, Entry.FileType);
|
|
}
|
|
} catch (Error) {
|
|
HandleNavigateDirectory("");
|
|
}
|
|
}
|
|
|
|
function HandleAuthenticated(User) {
|
|
SetCurrentUser(User);
|
|
SetAuthState("LoggedIn");
|
|
}
|
|
|
|
function HandleLoggedOut() {
|
|
SetCurrentUser(null);
|
|
SetAuthState("LoggedOut");
|
|
}
|
|
|
|
function HandleContinueAsGuest() {
|
|
SetCurrentUser(GuestUser);
|
|
SetAuthState("Guest");
|
|
}
|
|
|
|
function HandleRequestLogin() {
|
|
SetCurrentUser(null);
|
|
SetAuthState("LoggedOut");
|
|
}
|
|
|
|
function HandleProfileUpdated(UpdatedUser) {
|
|
SetCurrentUser((Existing) => ({ ...Existing, ...UpdatedUser }));
|
|
}
|
|
|
|
function HandleSettingsChanged(NewSettings) {
|
|
SetSettings(NewSettings);
|
|
ApplyFont(NewSettings);
|
|
FetchTheme(NewSettings.ThemeName).then(ApplyTheme);
|
|
SetSidebarKey((Key) => Key + 1);
|
|
}
|
|
|
|
function HandleSelectFile(FilePath, FileType) {
|
|
SetViewPath(FilePath);
|
|
SetViewIsDirectory(false);
|
|
SetViewFileType(FileType);
|
|
SetViewNoteFilePath(null);
|
|
SetCurrentDirectoryPath(GetParentPath(FilePath));
|
|
}
|
|
|
|
function HandleNavigateDirectory(DirectoryPath) {
|
|
SetViewPath(DirectoryPath);
|
|
SetViewIsDirectory(true);
|
|
SetViewFileType(null);
|
|
SetViewNoteFilePath(null);
|
|
SetCurrentDirectoryPath(DirectoryPath);
|
|
}
|
|
|
|
function HandleBrowseDirectory(DirectoryPath) {
|
|
SetCurrentDirectoryPath(DirectoryPath);
|
|
}
|
|
|
|
function HandleSelectDirectoryNote(DirectoryPath, NoteName, NoteFileType) {
|
|
SetViewPath(DirectoryPath);
|
|
SetViewIsDirectory(false);
|
|
SetViewFileType(NoteFileType);
|
|
SetViewNoteFilePath(`${DirectoryPath}/${NoteName}`);
|
|
SetCurrentDirectoryPath(DirectoryPath);
|
|
}
|
|
|
|
async function OpenDirectoryNote(DirectoryPath, Note) {
|
|
if (Note) {
|
|
HandleSelectDirectoryNote(DirectoryPath, Note.Name, Note.FileType);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const Created = await EnsureDirectoryNote(ActiveRoot, DirectoryPath);
|
|
HandleSelectDirectoryNote(DirectoryPath, Created.Name, Created.FileType);
|
|
} catch (Error) {
|
|
HandleNavigateDirectory(DirectoryPath);
|
|
}
|
|
}
|
|
|
|
function HandleBreadcrumbNavigate(Path) {
|
|
if (!Path) {
|
|
HandleNavigateDirectory("");
|
|
} else {
|
|
OpenDirectoryNote(Path, null);
|
|
}
|
|
}
|
|
|
|
function HandleOpenGraphNode(Node) {
|
|
SetIsGraphOpen(false);
|
|
if (Node.IsDirectory) {
|
|
HandleBreadcrumbNavigate(Node.Id);
|
|
} else {
|
|
HandleSelectFile(Node.Id, Node.FileType);
|
|
}
|
|
}
|
|
|
|
function HandleEntrySelect(EntryPath, IsDirectory, FileType, Note) {
|
|
if (IsDirectory) {
|
|
OpenDirectoryNote(EntryPath, Note);
|
|
} else {
|
|
HandleSelectFile(EntryPath, FileType);
|
|
}
|
|
}
|
|
|
|
function RefreshSidebar() {
|
|
SetSidebarKey((Key) => Key + 1);
|
|
}
|
|
|
|
function HandleEntryRenamed(OldPath, NewPath, IsDirectory) {
|
|
RefreshSidebar();
|
|
|
|
if (ViewNoteFilePath && ViewPath === OldPath) {
|
|
OpenDirectoryNote(NewPath, null);
|
|
} else if (!ViewIsDirectory && ViewPath === OldPath) {
|
|
HandleSelectFile(NewPath, ViewFileType);
|
|
} else if (ViewIsDirectory && ViewPath === OldPath) {
|
|
HandleNavigateDirectory(NewPath);
|
|
} else if (CurrentDirectoryPath === OldPath) {
|
|
SetCurrentDirectoryPath(NewPath);
|
|
}
|
|
}
|
|
|
|
function HandleEntryDeleted(DeletedPath) {
|
|
RefreshSidebar();
|
|
|
|
if (ViewPath === DeletedPath || ViewPath.startsWith(`${DeletedPath}/`)) {
|
|
HandleNavigateDirectory(GetParentPath(DeletedPath));
|
|
} else if (CurrentDirectoryPath === DeletedPath || CurrentDirectoryPath.startsWith(`${DeletedPath}/`)) {
|
|
SetCurrentDirectoryPath(GetParentPath(DeletedPath));
|
|
}
|
|
}
|
|
|
|
if (AuthState === "Loading") {
|
|
return null;
|
|
}
|
|
|
|
if (AuthState === "Bootstrap") {
|
|
return <LoginScreen IsBootstrap OnAuthenticated={HandleAuthenticated} />;
|
|
}
|
|
|
|
if (AuthState === "LoggedOut") {
|
|
return (
|
|
<LoginScreen
|
|
IsBootstrap={false}
|
|
OnAuthenticated={HandleAuthenticated}
|
|
OnContinueAsGuest={HandleContinueAsGuest}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const IsGuest = AuthState === "Guest";
|
|
const ShowTopBar = (Settings && Settings.ShowBreadcrumbs) || Roots.length > 1;
|
|
const ConfirmBeforeDelete = Settings ? Settings.ConfirmBeforeDelete : true;
|
|
|
|
return (
|
|
<div className="AppShell">
|
|
<Sidebar
|
|
key={SidebarKey}
|
|
RootName={ActiveRoot}
|
|
SelectedFile={ViewIsDirectory ? null : ViewPath}
|
|
CurrentDirectoryPath={CurrentDirectoryPath}
|
|
CurrentUser={CurrentUser}
|
|
IsGuest={IsGuest}
|
|
OnSelectFile={HandleSelectFile}
|
|
OnSelectDirectory={OpenDirectoryNote}
|
|
OnNavigateDirectory={HandleNavigateDirectory}
|
|
OnBrowseDirectory={HandleBrowseDirectory}
|
|
OnEntryRenamed={HandleEntryRenamed}
|
|
OnEntryDeleted={HandleEntryDeleted}
|
|
OnOpenSettings={() => SetIsSettingsOpen(true)}
|
|
OnToggleGraph={() => SetIsGraphOpen(!IsGraphOpen)}
|
|
IsGraphOpen={IsGraphOpen}
|
|
OnRequestLogin={HandleRequestLogin}
|
|
OnEntryCreated={RefreshSidebar}
|
|
UseHyphenBullets={Settings ? Settings.UseHyphenBullets : true}
|
|
UseAsciiFolderIcons={Settings ? Settings.UseAsciiFolderIcons : true}
|
|
NavigateIntoDirectories={Settings ? Settings.NavigateIntoDirectories : false}
|
|
NewEntryButtonsAtTop={Settings ? Settings.NewEntryButtonsAtTop : false}
|
|
ConfirmBeforeDelete={ConfirmBeforeDelete}
|
|
/>
|
|
<div className="MainColumn">
|
|
{ShowTopBar && (
|
|
<div className="TopBar">
|
|
{Settings && Settings.ShowBreadcrumbs ? (
|
|
<Breadcrumb
|
|
RootName={ActiveRoot}
|
|
FilePath={ViewPath}
|
|
IsDirectory={ViewIsDirectory || Boolean(ViewNoteFilePath)}
|
|
OnNavigate={HandleBreadcrumbNavigate}
|
|
/>
|
|
) : (
|
|
<span />
|
|
)}
|
|
{Roots.length > 1 && (
|
|
<select
|
|
className="RootSwitcher"
|
|
value={ActiveRoot || ""}
|
|
onChange={(Event) => SetActiveRoot(Event.target.value)}
|
|
>
|
|
{Roots.map((Root) => (
|
|
<option key={Root.Name} value={Root.Name}>
|
|
{Root.Name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
)}
|
|
</div>
|
|
)}
|
|
<div className="MainBody">
|
|
{IsGraphOpen ? (
|
|
<GraphPane
|
|
RootName={ActiveRoot}
|
|
DirectoryPath={CurrentDirectoryPath}
|
|
ShowDirectories={Settings ? Settings.GraphShowDirectories : true}
|
|
PanButton={Settings ? Settings.GraphPanButton : "Right"}
|
|
ColourByDepth={Settings ? Settings.GraphColourByDepth : true}
|
|
ShrinkByDepth={Settings ? Settings.GraphShrinkByDepth : false}
|
|
OnOpenNode={HandleOpenGraphNode}
|
|
OnClose={() => SetIsGraphOpen(false)}
|
|
/>
|
|
) : ViewIsDirectory ? (
|
|
<DirectoryPane
|
|
RootName={ActiveRoot}
|
|
DirectoryPath={ViewPath}
|
|
OnEntrySelect={HandleEntrySelect}
|
|
OnEntryCreated={RefreshSidebar}
|
|
OnEntryRenamed={HandleEntryRenamed}
|
|
OnEntryDeleted={HandleEntryDeleted}
|
|
UseHyphenBullets={Settings ? Settings.UseHyphenBullets : true}
|
|
UseAsciiFolderIcons={Settings ? Settings.UseAsciiFolderIcons : true}
|
|
ConfirmBeforeDelete={ConfirmBeforeDelete}
|
|
IsGuest={IsGuest}
|
|
/>
|
|
) : (
|
|
<PreviewPane
|
|
RootName={ActiveRoot}
|
|
FilePath={ViewNoteFilePath || ViewPath}
|
|
FileType={ViewFileType}
|
|
OnNavigateLink={HandleSelectFile}
|
|
ShowGitBlame={Settings ? Settings.ShowGitBlame : false}
|
|
ShowBlameAvatars={Settings ? Settings.ShowBlameAvatars : false}
|
|
StickyToolbar={Settings ? Settings.StickyToolbar : true}
|
|
MarkdownEditing={Settings ? Settings.MarkdownEditing : "Live"}
|
|
IsGuest={IsGuest}
|
|
/>
|
|
)}
|
|
{!IsGraphOpen && !ViewIsDirectory && !ViewNoteFilePath && (
|
|
<TagPanel RootName={ActiveRoot} FilePath={ViewPath} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
{IsSettingsOpen && (
|
|
<SettingsPanel
|
|
CurrentUser={CurrentUser}
|
|
OnClose={() => SetIsSettingsOpen(false)}
|
|
OnSettingsChanged={HandleSettingsChanged}
|
|
OnLoggedOut={HandleLoggedOut}
|
|
OnProfileUpdated={HandleProfileUpdated}
|
|
/>
|
|
)}
|
|
<ErrorToast Message={ErrorMessage} OnDismiss={() => SetErrorMessage(null)} />
|
|
</div>
|
|
);
|
|
}
|