Files
SaraView/Frontend/src/App.jsx
T

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