mirror of
https://git.srhtnin.garden/sarah/SaraView.git
synced 2026-10-15 02:43:17 +02:00
Added .sv_* files that are files for directories. So you can now edit
directories as files.
This commit is contained in:
1 parent
70a390f7cf
commit
6083e00504
9 files changed
+176
-18
No files matched your search
+76
-3
@@ -38,6 +38,64 @@ def ClassifyFile(EntryPath: Path) -> str:
|
||||
return FileTypeMap.get(EntryPath.suffix.lower(), "unknown")
|
||||
|
||||
|
||||
NoteFilePrefix = ".sv_"
|
||||
DefaultNoteExtension = ".md"
|
||||
|
||||
|
||||
def IsNoteFile(EntryName: str) -> bool:
|
||||
"""A directory's own "file" - editable/viewable like any other file, but
|
||||
hidden from normal listings behind the ShowSvFiles setting.
|
||||
"""
|
||||
return EntryName.startswith(NoteFilePrefix)
|
||||
|
||||
|
||||
def FindDirectoryNote(DirectoryPath: Path) -> Path | None:
|
||||
"""Find a directory's note file, whatever extension it currently has."""
|
||||
Prefix = f"{NoteFilePrefix}{DirectoryPath.name}."
|
||||
|
||||
for Candidate in DirectoryPath.iterdir():
|
||||
if Candidate.is_file() and Candidate.name.startswith(Prefix):
|
||||
return Candidate
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def EnsureDirectoryNote(DirectoryPath: Path) -> Path:
|
||||
"""Find a directory's note file, creating one (defaulting to markdown)
|
||||
if it doesn't have one yet - e.g. a directory that predates this feature.
|
||||
"""
|
||||
Existing = FindDirectoryNote(DirectoryPath)
|
||||
if Existing is not None:
|
||||
return Existing
|
||||
|
||||
NotePath = DirectoryPath / f"{NoteFilePrefix}{DirectoryPath.name}{DefaultNoteExtension}"
|
||||
NotePath.touch()
|
||||
return NotePath
|
||||
|
||||
|
||||
def GetDirectoryNoteInfo(DirectoryPath: Path) -> dict[str, Any] | None:
|
||||
"""Read-only lookup for listings - never creates a note."""
|
||||
NotePath = FindDirectoryNote(DirectoryPath)
|
||||
|
||||
if NotePath is None:
|
||||
return None
|
||||
|
||||
return {"Name": NotePath.name, "FileType": ClassifyFile(NotePath)}
|
||||
|
||||
|
||||
def RenameDirectoryNote(DirectoryPath: Path, OldDirectoryName: str) -> None:
|
||||
"""After a directory is renamed/moved, keep its note file's name (which
|
||||
embeds the directory's own name) in sync, preserving its extension.
|
||||
"""
|
||||
OldPrefix = f"{NoteFilePrefix}{OldDirectoryName}."
|
||||
|
||||
for Candidate in DirectoryPath.iterdir():
|
||||
if Candidate.is_file() and Candidate.name.startswith(OldPrefix):
|
||||
NewName = f"{NoteFilePrefix}{DirectoryPath.name}{Candidate.suffix}"
|
||||
Candidate.rename(DirectoryPath / NewName)
|
||||
return
|
||||
|
||||
|
||||
def ListRoots() -> list[dict[str, Any]]:
|
||||
"""Return the configured roots as a flat list for the frontend."""
|
||||
return [{"Name": Name} for Name in GetRoots()]
|
||||
@@ -52,6 +110,7 @@ def CreateEntry(RootName: str, RelativePath: str, IsDirectory: bool) -> None:
|
||||
|
||||
if IsDirectory:
|
||||
EntryPath.mkdir(parents=False)
|
||||
EnsureDirectoryNote(EntryPath)
|
||||
else:
|
||||
EntryPath.parent.mkdir(parents=True, exist_ok=True)
|
||||
EntryPath.touch()
|
||||
@@ -70,6 +129,7 @@ def MoveEntry(RootName: str, SourceRelativePath: str, DestinationRelativePath: s
|
||||
if DestinationPath.exists():
|
||||
raise FileExistsError(f"{DestinationPath.name} already exists.")
|
||||
|
||||
OldName = SourcePath.name
|
||||
DestinationPath.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
@@ -78,6 +138,9 @@ def MoveEntry(RootName: str, SourceRelativePath: str, DestinationRelativePath: s
|
||||
# Cross-device or other rename failures — fall back to a copy+delete.
|
||||
shutil.move(str(SourcePath), str(DestinationPath))
|
||||
|
||||
if DestinationPath.is_dir():
|
||||
RenameDirectoryNote(DestinationPath, OldName)
|
||||
|
||||
|
||||
def DeleteEntry(RootName: str, RelativePath: str) -> None:
|
||||
"""Delete a file, or an entire directory tree."""
|
||||
@@ -92,11 +155,17 @@ def DeleteEntry(RootName: str, RelativePath: str) -> None:
|
||||
EntryPath.unlink()
|
||||
|
||||
|
||||
def ListDirectory(RootName: str, RelativePath: str, ShowHidden: bool = False) -> list[dict[str, Any]]:
|
||||
def ListDirectory(
|
||||
RootName: str, RelativePath: str, ShowHidden: bool = False, ShowSvFiles: bool = False
|
||||
) -> list[dict[str, Any]]:
|
||||
"""List the immediate contents of a directory.
|
||||
|
||||
SaraTag's internal files are always hidden, regardless of ShowHidden.
|
||||
Dotfiles/dotfolders are hidden unless ShowHidden is True.
|
||||
Note files (.sv_*) are hidden unless ShowSvFiles is True - independently
|
||||
of ShowHidden, since they're an implementation detail rather than a
|
||||
normal dotfile. Every directory's Note metadata is still attached
|
||||
regardless of ShowSvFiles, so clicking the directory can open it.
|
||||
Other dotfiles/dotfolders are hidden unless ShowHidden is True.
|
||||
"""
|
||||
DirectoryPath = ResolvePath(RootName, RelativePath)
|
||||
|
||||
@@ -114,7 +183,10 @@ def ListDirectory(RootName: str, RelativePath: str, ShowHidden: bool = False) ->
|
||||
if IsInternalPath(EntryPath):
|
||||
continue
|
||||
|
||||
if not ShowHidden and EntryPath.name.startswith("."):
|
||||
if IsNoteFile(EntryPath.name):
|
||||
if not ShowSvFiles:
|
||||
continue
|
||||
elif not ShowHidden and EntryPath.name.startswith("."):
|
||||
continue
|
||||
|
||||
IsDirectory = EntryPath.is_dir()
|
||||
@@ -124,6 +196,7 @@ def ListDirectory(RootName: str, RelativePath: str, ShowHidden: bool = False) ->
|
||||
"IsDirectory": IsDirectory,
|
||||
"FileType": None if IsDirectory else ClassifyFile(EntryPath),
|
||||
"Size": None if IsDirectory else EntryPath.stat().st_size,
|
||||
"Note": GetDirectoryNoteInfo(EntryPath) if IsDirectory else None,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
+32
-3
@@ -18,7 +18,15 @@ from BookReader import ReadBookForFile
|
||||
from Blame import LoadBlame, RenameUserInBlame, UpdateBlameOnSave
|
||||
from Core import ResolvePath
|
||||
from Editor import WriteTextFile
|
||||
from Explorer import ClassifyFile, CreateEntry, DeleteEntry, ListDirectory, ListRoots, MoveEntry
|
||||
from Explorer import (
|
||||
ClassifyFile,
|
||||
CreateEntry,
|
||||
DeleteEntry,
|
||||
EnsureDirectoryNote,
|
||||
ListDirectory,
|
||||
ListRoots,
|
||||
MoveEntry,
|
||||
)
|
||||
from Groups import (
|
||||
CreateGroup,
|
||||
DeleteGroup,
|
||||
@@ -378,8 +386,10 @@ def GetTreeEndpoint(
|
||||
RequirePathAccessible(Root, Path, CurrentUser)
|
||||
|
||||
try:
|
||||
ShowHidden = LoadSettings(CurrentUser["Username"]).get("ShowHiddenFolders", False)
|
||||
Entries = ListDirectory(Root, Path, ShowHidden)
|
||||
UserSettings = LoadSettings(CurrentUser["Username"])
|
||||
ShowHidden = UserSettings.get("ShowHiddenFolders", False)
|
||||
ShowSvFiles = UserSettings.get("ShowSvFiles", False)
|
||||
Entries = ListDirectory(Root, Path, ShowHidden, ShowSvFiles)
|
||||
except (ValueError, NotADirectoryError, FileNotFoundError) as Error:
|
||||
raise HTTPException(status_code=400, detail=str(Error))
|
||||
|
||||
@@ -400,6 +410,25 @@ def GetTreeEndpoint(
|
||||
]
|
||||
|
||||
|
||||
@Application.post("/api/note")
|
||||
def PostNoteEndpoint(Root: str, Path: str, CurrentUser: dict = Depends(GetCurrentUser)) -> dict:
|
||||
"""Find (or create, for a directory that predates this feature) the
|
||||
directory's own note file, so it can be opened like any other file.
|
||||
"""
|
||||
RequirePathAccessible(Root, Path, CurrentUser)
|
||||
|
||||
try:
|
||||
DirectoryPath = ResolvePath(Root, Path)
|
||||
except ValueError as Error:
|
||||
raise HTTPException(status_code=400, detail=str(Error))
|
||||
|
||||
if not DirectoryPath.is_dir():
|
||||
raise HTTPException(status_code=400, detail="Not a directory.")
|
||||
|
||||
NotePath = EnsureDirectoryNote(DirectoryPath)
|
||||
return {"Name": NotePath.name, "FileType": ClassifyFile(NotePath)}
|
||||
|
||||
|
||||
class CreateEntryRequest(BaseModel):
|
||||
IsDirectory: bool
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ DefaultSettings: dict[str, Any] = {
|
||||
"FontSize": 14,
|
||||
"ThemeName": "CatppuccinMocha",
|
||||
"ShowHiddenFolders": False,
|
||||
"ShowSvFiles": False,
|
||||
"ShowBreadcrumbs": True,
|
||||
"UseHyphenBullets": True,
|
||||
"UseAsciiFolderIcons": True,
|
||||
|
||||
Reference in new issue
Block a user