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