mirror of
https://git.srhtnin.garden/sarah/SaraView.git
synced 2026-10-13 18:28:14 +02:00
168 lines
7.2 KiB
Python
168 lines
7.2 KiB
Python
"""What the window's parts share: the config, the current Source (local or
|
|
server), toasts, dialogs and the login flow. Changing the source (Settings,
|
|
logging in or out) tells every listener to reload."""
|
|
|
|
from typing import Callable, Optional
|
|
|
|
import flet as ft
|
|
|
|
from Core import AppConfig, ChipButton, LoadSessionToken, NotificationStyle, Palette, SaveSessionToken
|
|
from Sources import MakeSource, ServerSource, Source, SourceError
|
|
|
|
|
|
class App:
|
|
def __init__(self, Page: ft.Page, Config: AppConfig):
|
|
self.Page = Page
|
|
self.Config = Config
|
|
self.Picker = ft.FilePicker() # registers itself with the page
|
|
self.Source: Source = MakeSource(Config)
|
|
self.ConnectError = "" # why the server couldn't be reached, shown instead of the file list
|
|
self.DialogOpen = False # keyboard shortcuts pause while a dialog is up
|
|
self.Listeners: list[Callable[[], None]] = [] # run after the source changes
|
|
|
|
# --- Source -----------------------------------------------------------
|
|
|
|
def Connect(self) -> None:
|
|
"""(Re)builds the source from Config and, for a server, checks it's
|
|
there and whether the saved login still works. Listeners reload."""
|
|
if isinstance(self.Source, ServerSource):
|
|
self.Source.Close()
|
|
Token = LoadSessionToken(self.Config.ServerUrl) if self.Config.Mode == "Server" and self.Config.RememberLogin else ""
|
|
self.Source = MakeSource(self.Config, Token)
|
|
self.ConnectError = ""
|
|
if isinstance(self.Source, ServerSource):
|
|
try:
|
|
self.Source.Connect()
|
|
except SourceError as Error:
|
|
self.ConnectError = str(Error)
|
|
if Token and self.Source.User is None and not self.ConnectError:
|
|
SaveSessionToken(self.Config.ServerUrl, "")
|
|
self.Notify("Your saved login ended (the server restarted?). Browsing as Guest.", "warning")
|
|
self.Changed()
|
|
|
|
def Changed(self) -> None:
|
|
for Listener in self.Listeners:
|
|
Listener()
|
|
|
|
@property
|
|
def Server(self) -> Optional[ServerSource]:
|
|
return self.Source if isinstance(self.Source, ServerSource) else None
|
|
|
|
def HandleError(self, Error: SourceError, Doing: str = "") -> None:
|
|
"""A toast, plus the login dialog when the server wants a login."""
|
|
Text = f"{Doing}: {Error}" if Doing else str(Error)
|
|
if Error.Status == 401 and self.Server is not None:
|
|
if self.Server.User is not None: # the session ended under us
|
|
self.Server.Logout()
|
|
SaveSessionToken(self.Config.ServerUrl, "")
|
|
self.Changed()
|
|
self.ShowLoginDialog(Reason=Text)
|
|
return
|
|
self.Notify(Text, "error")
|
|
|
|
# --- Login ------------------------------------------------------------
|
|
|
|
def ShowLoginDialog(self, Reason: str = "") -> None:
|
|
Server = self.Server
|
|
if Server is None:
|
|
return
|
|
UsernameField = ft.TextField(label="Username", value=self.Config.Username, autofocus=not self.Config.Username)
|
|
PasswordField = ft.TextField(label="Password", password=True, can_reveal_password=True,
|
|
autofocus=bool(self.Config.Username))
|
|
RememberSwitch = ft.Switch(label="Stay logged in", value=self.Config.RememberLogin)
|
|
ErrorText = ft.Text("", color=Palette.Red, size=12, visible=False)
|
|
|
|
def DoLogin(e=None) -> None:
|
|
Username = (UsernameField.value or "").strip()
|
|
if not Username or not PasswordField.value:
|
|
return
|
|
try:
|
|
Server.Login(Username, PasswordField.value)
|
|
except SourceError as Error:
|
|
ErrorText.value = str(Error)
|
|
ErrorText.visible = True
|
|
ErrorText.update()
|
|
return
|
|
self.Config.Username = Username
|
|
self.Config.RememberLogin = RememberSwitch.value
|
|
self.Config.Save()
|
|
SaveSessionToken(self.Config.ServerUrl, Server.Token if RememberSwitch.value else "")
|
|
self.Close()
|
|
self.Notify(f"Logged in as {Username}.", "success")
|
|
self.Changed()
|
|
|
|
UsernameField.on_submit = lambda e: PasswordField.focus()
|
|
PasswordField.on_submit = DoLogin
|
|
Content = [ft.Text(Server.Url, color=Palette.Overlay0, size=12)]
|
|
if Reason:
|
|
Content.append(ft.Text(Reason, color=Palette.Yellow, size=13))
|
|
Content += [UsernameField, PasswordField, RememberSwitch, ErrorText]
|
|
self.ShowDialog("Log in", ft.Column(Content, tight=True, spacing=12), [
|
|
ChipButton("Cancel", lambda e: self.Close()),
|
|
ChipButton("Log in", DoLogin),
|
|
], Width=380)
|
|
|
|
def Logout(self) -> None:
|
|
if self.Server is None:
|
|
return
|
|
self.Server.Logout()
|
|
SaveSessionToken(self.Config.ServerUrl, "")
|
|
self.Notify("Logged out. Browsing as Guest.", "info")
|
|
self.Changed()
|
|
|
|
# --- UI helpers -------------------------------------------------------
|
|
|
|
def Notify(self, Text: str, Kind: str = "info") -> None:
|
|
Background, Accent = NotificationStyle(Kind)
|
|
self.Page.show_dialog(ft.SnackBar(
|
|
content=ft.Text(Text, color=Accent),
|
|
bgcolor=Background,
|
|
shape=ft.RoundedRectangleBorder(radius=4, side=ft.BorderSide(1, Accent)),
|
|
open=True,
|
|
))
|
|
|
|
def ShowDialog(self, Title: str, Content: ft.Control, Actions: list[ft.Control], Width: int = 440) -> None:
|
|
self.DialogOpen = True
|
|
self.Page.show_dialog(ft.AlertDialog(
|
|
title=Title,
|
|
content=ft.Container(Content, width=Width),
|
|
actions=Actions,
|
|
on_dismiss=lambda e: setattr(self, "DialogOpen", False),
|
|
))
|
|
|
|
def Close(self) -> None:
|
|
self.DialogOpen = False
|
|
self.Page.pop_dialog()
|
|
|
|
def Confirm(self, Title: str, Text: str, OnYes: Callable[[], None], YesText: str = "Delete") -> None:
|
|
def Yes(e) -> None:
|
|
self.Close()
|
|
OnYes()
|
|
|
|
self.ShowDialog(Title, ft.Text(Text), [
|
|
ChipButton("Cancel", lambda e: self.Close()),
|
|
ChipButton(YesText, Yes, Accent=Palette.Red),
|
|
])
|
|
|
|
def AskName(self, Title: str, Value: str, SelectEnd: int, OnDone: Callable[[str], None],
|
|
ActionText: str = "Create", Note: str = "", RequireChange: bool = True) -> None:
|
|
"""A one-field dialog with the name's stem preselected, like SaraFem's.
|
|
RequireChange: an unchanged name does nothing (Rename)."""
|
|
NameField = ft.TextField(value=Value, autofocus=True,
|
|
selection=ft.TextSelection(base_offset=0, extent_offset=SelectEnd))
|
|
|
|
def Done(e=None) -> None:
|
|
NewName = (NameField.value or "").strip()
|
|
self.Close()
|
|
if NewName and (NewName != Value or not RequireChange):
|
|
OnDone(NewName)
|
|
|
|
NameField.on_submit = Done
|
|
Content: ft.Control = NameField
|
|
if Note:
|
|
Content = ft.Column([NameField, ft.Text(Note, color=Palette.Overlay0, size=12)], tight=True, spacing=8)
|
|
self.ShowDialog(Title, Content, [
|
|
ChipButton("Cancel", lambda e: self.Close()),
|
|
ChipButton(ActionText, Done),
|
|
], Width=380)
|