Add pages abstraction
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
# Soul Diary
|
||||
|
||||
## ToDo
|
||||
|
||||
1. Refactoring: create separate pages and user controls
|
||||
2. Implement S3 backend client
|
||||
3. Implement FTP backend client
|
||||
|
||||
## User Flow
|
||||
|
||||
### Soul Diary Server
|
||||
|
||||
@@ -3,7 +3,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class APISettings(BaseSettings):
|
||||
model_config = SettingsConfigDict(prefix="backend_api_")
|
||||
model_config = SettingsConfigDict(env_prefix="backend_api_")
|
||||
|
||||
port: conint(ge=1, le=65535) = 8001
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class DatabaseSettings(BaseSettings):
|
||||
model_config = SettingsConfigDict(prefix="backend_database_")
|
||||
model_config = SettingsConfigDict(env_prefix="backend_database_")
|
||||
|
||||
dsn: AnyUrl = "sqlite+aiosqlite:///soul_diary.sqlite3"
|
||||
|
||||
@@ -9,7 +9,6 @@ from soul_diary.ui.app.routes import AUTH, SENSE_LIST
|
||||
async def middleware(page: flet.Page, params: Params, basket: Basket):
|
||||
local_storage = LocalStorage(client_storage=page.client_storage)
|
||||
auth_data = await local_storage.get_auth_data()
|
||||
# await local_storage._client_storage.clear_async()
|
||||
if auth_data is None:
|
||||
await page.go_async(AUTH)
|
||||
return
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
from typing import Callable
|
||||
|
||||
import flet
|
||||
from flet_route import Basket, Params
|
||||
|
||||
|
||||
class BaseMiddleware:
|
||||
async def __call__(
|
||||
self,
|
||||
page: flet.Page,
|
||||
params: Params,
|
||||
basket: Basket,
|
||||
next_handler: Callable,
|
||||
):
|
||||
raise NotImplementedError
|
||||
61
soul_diary/ui/app/pages/auth/choose_backend.py
Normal file
61
soul_diary/ui/app/pages/auth/choose_backend.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from functools import partial
|
||||
|
||||
import flet
|
||||
|
||||
from soul_diary.ui.app.models import BackendType
|
||||
from soul_diary.ui.app.pages.base import BasePage
|
||||
|
||||
|
||||
class ChooseBackendPage(BasePage):
|
||||
BACKENDS = {
|
||||
BackendType.LOCAL: "Локально",
|
||||
BackendType.SOUL: "Soul Diary сервер",
|
||||
}
|
||||
|
||||
def __init__(self, backend: BackendType | None = None):
|
||||
self.backend: BackendType | None = backend
|
||||
|
||||
super().__init__()
|
||||
|
||||
def build(self) -> flet.Container:
|
||||
label = flet.Container(
|
||||
content=flet.Text("Выберите сервер"),
|
||||
alignment=flet.alignment.center,
|
||||
)
|
||||
dropdown = flet.Dropdown(
|
||||
label="Бэкенд",
|
||||
options=[
|
||||
flet.dropdown.Option(text=text, key=key.value)
|
||||
for key, text in self.BACKENDS.items()
|
||||
],
|
||||
value=None if self.backend is None else self.backend.value,
|
||||
on_change=self.callback_change_backend,
|
||||
)
|
||||
connect_button = flet.ElevatedButton(
|
||||
"Выбрать",
|
||||
width=300,
|
||||
height=50,
|
||||
on_click=partial(self.callback_choose_backend, dropdown=dropdown),
|
||||
)
|
||||
|
||||
return flet.Container(
|
||||
content=flet.Column(
|
||||
controls=[label, dropdown, connect_button],
|
||||
width=300,
|
||||
),
|
||||
alignment=flet.alignment.center,
|
||||
)
|
||||
|
||||
async def callback_change_backend(self, event: flet.ControlEvent):
|
||||
self.backend = BackendType(event.control.value)
|
||||
event.control.error_text = None
|
||||
await event.control.update_async()
|
||||
|
||||
async def callback_choose_backend(self, event: flet.ControlEvent, dropdown: flet.Dropdown):
|
||||
if self.backend == BackendType.LOCAL:
|
||||
await self.credentials_view(page=event.page)
|
||||
elif self.backend == BackendType.SOUL:
|
||||
await self.soul_server_data_view(page=event.page)
|
||||
else:
|
||||
dropdown.error_text = "Выберите тип бекенда"
|
||||
await self.update_async()
|
||||
8
soul_diary/ui/app/pages/auth/soul_backend_data.py
Normal file
8
soul_diary/ui/app/pages/auth/soul_backend_data.py
Normal file
@@ -0,0 +1,8 @@
|
||||
import flet
|
||||
|
||||
from soul_diary.ui.app.pages.base import BasePage
|
||||
|
||||
|
||||
class SoulBackendDataPage(BasePage):
|
||||
def build(self) -> flet.Container:
|
||||
pass
|
||||
7
soul_diary/ui/app/pages/base.py
Normal file
7
soul_diary/ui/app/pages/base.py
Normal file
@@ -0,0 +1,7 @@
|
||||
import flet
|
||||
|
||||
|
||||
class BasePage(flet.UserControl):
|
||||
async def apply(self, page: flet.Page):
|
||||
await page.clean_async()
|
||||
await page.add_async(self)
|
||||
@@ -1,6 +1,6 @@
|
||||
import asyncio
|
||||
from functools import partial
|
||||
from typing import Any, Callable, Sequence
|
||||
from typing import Any
|
||||
|
||||
import flet
|
||||
from pydantic import AnyHttpUrl
|
||||
@@ -8,7 +8,6 @@ from pydantic import AnyHttpUrl
|
||||
from soul_diary.ui.app.backend.exceptions import IncorrectCredentialsException, UserAlreadyExistsException
|
||||
from soul_diary.ui.app.backend.soul import SoulBackend
|
||||
from soul_diary.ui.app.local_storage import LocalStorage
|
||||
from soul_diary.ui.app.middlewares.base import BaseMiddleware
|
||||
from soul_diary.ui.app.models import BackendType, Options
|
||||
from soul_diary.ui.app.routes import AUTH, SENSE_LIST
|
||||
from soul_diary.ui.app.views.exceptions import SoulServerIncorrectURL
|
||||
@@ -21,7 +20,6 @@ class AuthView(BaseView):
|
||||
local_storage: LocalStorage,
|
||||
backend: BackendType | None = None,
|
||||
backend_data: dict[str, Any] | None = None,
|
||||
middlewares: Sequence[BaseMiddleware | Callable] = (),
|
||||
):
|
||||
self.top_container: flet.Container
|
||||
self.center_container: flet.Container
|
||||
@@ -33,7 +31,7 @@ class AuthView(BaseView):
|
||||
self.username: str | None = None
|
||||
self.password: str | None = None
|
||||
|
||||
super().__init__(local_storage=local_storage, middlewares=middlewares)
|
||||
super().__init__(local_storage=local_storage)
|
||||
|
||||
async def clear(self):
|
||||
self.top_container.content = None
|
||||
@@ -81,7 +79,6 @@ class AuthView(BaseView):
|
||||
label = flet.Text("Выберите сервер")
|
||||
self.top_container.content = label
|
||||
|
||||
backend_controls = flet.Column()
|
||||
backend_dropdown = flet.Dropdown(
|
||||
label="Бэкенд",
|
||||
options=[
|
||||
@@ -91,14 +88,7 @@ class AuthView(BaseView):
|
||||
value=None if self.backend is None else self.backend.value,
|
||||
on_change=self.callback_change_backend,
|
||||
)
|
||||
|
||||
container = flet.Container(
|
||||
content=flet.Column(
|
||||
controls=[backend_dropdown, backend_controls],
|
||||
width=300,
|
||||
),
|
||||
)
|
||||
self.center_container.content = container
|
||||
self.center_container.content = backend_dropdown
|
||||
|
||||
connect_button = flet.ElevatedButton(
|
||||
"Выбрать",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
from functools import partial, reduce
|
||||
from typing import Any, Callable, Sequence
|
||||
from typing import Any, Callable
|
||||
|
||||
import flet
|
||||
from flet_route import Basket, Params
|
||||
@@ -10,7 +10,6 @@ from soul_diary.ui.app.backend.exceptions import NonAuthenticatedException
|
||||
from soul_diary.ui.app.backend.local import LocalBackend
|
||||
from soul_diary.ui.app.backend.soul import SoulBackend
|
||||
from soul_diary.ui.app.local_storage import LocalStorage
|
||||
from soul_diary.ui.app.middlewares.base import BaseMiddleware
|
||||
from soul_diary.ui.app.models import BackendType
|
||||
|
||||
|
||||
@@ -29,21 +28,6 @@ def view(initial: bool = False, disabled: bool = False):
|
||||
return decorator
|
||||
|
||||
|
||||
def setup_middlewares(function: Callable):
|
||||
async def wrapper(self, page: flet.Page, params: Params, basket: Basket) -> flet.View:
|
||||
entrypoint = partial(function, self)
|
||||
if self.middlewares:
|
||||
entrypoint = partial(self.middlewares[-1], next_handler=entrypoint)
|
||||
entrypoint = reduce(
|
||||
lambda entrypoint, function: partial(entrypoint, next_handler=function),
|
||||
self.middlewares[-2::-1],
|
||||
entrypoint,
|
||||
)
|
||||
return await entrypoint(page=page, params=params, basket=basket)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class MetaView(type):
|
||||
def __init__(cls, name: str, bases: tuple[type], attrs: dict[str, Any]):
|
||||
super().__init__(name, bases, attrs)
|
||||
@@ -76,28 +60,23 @@ class BaseView(metaclass=MetaView):
|
||||
is_abstract = True
|
||||
_initial_view: Callable | None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
local_storage: LocalStorage,
|
||||
middlewares: Sequence[BaseMiddleware | Callable] = (),
|
||||
):
|
||||
def __init__(self, local_storage: LocalStorage):
|
||||
self.local_storage = local_storage
|
||||
self.middlewares = middlewares
|
||||
|
||||
self.container: flet.Container
|
||||
self.stack: flet.Stack
|
||||
self.view: flet.View
|
||||
|
||||
@setup_middlewares
|
||||
async def entrypoint(self, page: flet.Page, params: Params, basket: Basket) -> flet.View:
|
||||
self.container = flet.Container()
|
||||
self.stack = flet.Stack(controls=[self.container])
|
||||
self.view = flet.View(controls=[self.stack], route="/test")
|
||||
self.view = flet.View(controls=[self.container])
|
||||
|
||||
await self.setup()
|
||||
await self.clear()
|
||||
self.clear_data()
|
||||
await self.run_initial_view(page=page)
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
loop.create_task(self.run_initial_view(page=page))
|
||||
|
||||
return self.view
|
||||
|
||||
async def setup(self):
|
||||
@@ -111,21 +90,20 @@ class BaseView(metaclass=MetaView):
|
||||
|
||||
@asynccontextmanager
|
||||
async def in_progress(self, page: flet.Page, tooltip: str | None = None):
|
||||
for control in self.stack.controls:
|
||||
control.disabled = True
|
||||
self.stack.controls.append(
|
||||
flet.Container(
|
||||
self.container.disabled = True
|
||||
page.splash = flet.Column(
|
||||
controls=[flet.Container(
|
||||
content=flet.ProgressRing(tooltip=tooltip),
|
||||
alignment=flet.alignment.center,
|
||||
),
|
||||
)],
|
||||
alignment=flet.MainAxisAlignment.CENTER,
|
||||
)
|
||||
await page.update_async()
|
||||
|
||||
yield
|
||||
|
||||
self.stack.controls.pop()
|
||||
for control in self.stack.controls:
|
||||
control.disabled = False
|
||||
self.container.disabled = False
|
||||
page.splash = None
|
||||
await page.update_async()
|
||||
|
||||
async def run_initial_view(self, page: flet.Page):
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
from functools import partial
|
||||
from typing import Awaitable, Callable, Sequence
|
||||
|
||||
import flet
|
||||
|
||||
from soul_diary.ui.app.local_storage import LocalStorage
|
||||
from soul_diary.ui.app.middlewares.base import BaseMiddleware
|
||||
from soul_diary.ui.app.models import Emotion
|
||||
from soul_diary.ui.app.routes import SENSE_ADD, SENSE_LIST
|
||||
from .base import BaseView, view
|
||||
@@ -14,7 +12,6 @@ class SenseAddView(BaseView):
|
||||
def __init__(
|
||||
self,
|
||||
local_storage: LocalStorage,
|
||||
middlewares: Sequence[BaseMiddleware | Callable[[flet.Page], Awaitable]] = (),
|
||||
):
|
||||
self.title: flet.Text
|
||||
self.content_container: flet.Container
|
||||
@@ -25,7 +22,7 @@ class SenseAddView(BaseView):
|
||||
self.body: str | None = None
|
||||
self.desires: str | None = None
|
||||
|
||||
super().__init__(local_storage=local_storage, middlewares=middlewares)
|
||||
super().__init__(local_storage=local_storage)
|
||||
|
||||
async def clear(self):
|
||||
self.title.value = ""
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import asyncio
|
||||
from typing import Awaitable, Callable, Sequence
|
||||
|
||||
import flet
|
||||
from soul_diary.ui.app.backend.exceptions import NonAuthenticatedException
|
||||
|
||||
from soul_diary.ui.app.local_storage import LocalStorage
|
||||
from soul_diary.ui.app.middlewares.base import BaseMiddleware
|
||||
from soul_diary.ui.app.models import BackendType, Sense
|
||||
from soul_diary.ui.app.models import Sense
|
||||
from soul_diary.ui.app.routes import AUTH, SENSE_ADD, SENSE_LIST
|
||||
from .base import BaseView, view
|
||||
|
||||
@@ -15,13 +13,12 @@ class SenseListView(BaseView):
|
||||
def __init__(
|
||||
self,
|
||||
local_storage: LocalStorage,
|
||||
middlewares: Sequence[BaseMiddleware | Callable[[flet.Page], Awaitable]] = (),
|
||||
):
|
||||
self.cards: flet.Column
|
||||
|
||||
self.local_storage = local_storage
|
||||
|
||||
super().__init__(local_storage=local_storage, middlewares=middlewares)
|
||||
super().__init__(local_storage=local_storage)
|
||||
|
||||
async def setup(self):
|
||||
self.cards = flet.Column(alignment=flet.alignment.center, width=400)
|
||||
@@ -97,5 +94,6 @@ class SenseListView(BaseView):
|
||||
|
||||
async def callback_logout(self, event: flet.ControlEvent):
|
||||
backend_client = await self.get_backend_client()
|
||||
await backend_client.logout()
|
||||
async with self.in_progress(page=event.page):
|
||||
await backend_client.logout()
|
||||
await event.page.go_async(AUTH)
|
||||
|
||||
@@ -25,8 +25,9 @@ class WebService(ServiceMixin):
|
||||
|
||||
async def start(self):
|
||||
app = flet_fastapi.app(SoulDiaryApp(
|
||||
backend=BackendType.SOUL,
|
||||
backend_data=self._backend_data,
|
||||
# backend=BackendType.SOUL,
|
||||
# backend_data=self._backend_data,
|
||||
# backend=BackendType.LOCAL,
|
||||
).run)
|
||||
config = uvicorn.Config(app=app, host="0.0.0.0", port=self._port)
|
||||
server = UvicornServer(config)
|
||||
|
||||
Reference in New Issue
Block a user