Add pages abstraction

This commit is contained in:
2023-12-14 22:11:10 +03:00
parent 62ec2d52f8
commit 7f4aeb921e
14 changed files with 110 additions and 80 deletions

View File

@@ -1,5 +1,11 @@
# Soul Diary # Soul Diary
## ToDo
1. Refactoring: create separate pages and user controls
2. Implement S3 backend client
3. Implement FTP backend client
## User Flow ## User Flow
### Soul Diary Server ### Soul Diary Server

View File

@@ -3,7 +3,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
class APISettings(BaseSettings): class APISettings(BaseSettings):
model_config = SettingsConfigDict(prefix="backend_api_") model_config = SettingsConfigDict(env_prefix="backend_api_")
port: conint(ge=1, le=65535) = 8001 port: conint(ge=1, le=65535) = 8001

View File

@@ -3,6 +3,6 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
class DatabaseSettings(BaseSettings): class DatabaseSettings(BaseSettings):
model_config = SettingsConfigDict(prefix="backend_database_") model_config = SettingsConfigDict(env_prefix="backend_database_")
dsn: AnyUrl = "sqlite+aiosqlite:///soul_diary.sqlite3" dsn: AnyUrl = "sqlite+aiosqlite:///soul_diary.sqlite3"

View File

@@ -9,7 +9,6 @@ from soul_diary.ui.app.routes import AUTH, SENSE_LIST
async def middleware(page: flet.Page, params: Params, basket: Basket): async def middleware(page: flet.Page, params: Params, basket: Basket):
local_storage = LocalStorage(client_storage=page.client_storage) local_storage = LocalStorage(client_storage=page.client_storage)
auth_data = await local_storage.get_auth_data() auth_data = await local_storage.get_auth_data()
# await local_storage._client_storage.clear_async()
if auth_data is None: if auth_data is None:
await page.go_async(AUTH) await page.go_async(AUTH)
return return

View File

@@ -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

View 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()

View 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

View 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)

View File

@@ -1,6 +1,6 @@
import asyncio import asyncio
from functools import partial from functools import partial
from typing import Any, Callable, Sequence from typing import Any
import flet import flet
from pydantic import AnyHttpUrl 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.exceptions import IncorrectCredentialsException, UserAlreadyExistsException
from soul_diary.ui.app.backend.soul import SoulBackend from soul_diary.ui.app.backend.soul import SoulBackend
from soul_diary.ui.app.local_storage import LocalStorage 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.models import BackendType, Options
from soul_diary.ui.app.routes import AUTH, SENSE_LIST from soul_diary.ui.app.routes import AUTH, SENSE_LIST
from soul_diary.ui.app.views.exceptions import SoulServerIncorrectURL from soul_diary.ui.app.views.exceptions import SoulServerIncorrectURL
@@ -21,7 +20,6 @@ class AuthView(BaseView):
local_storage: LocalStorage, local_storage: LocalStorage,
backend: BackendType | None = None, backend: BackendType | None = None,
backend_data: dict[str, Any] | None = None, backend_data: dict[str, Any] | None = None,
middlewares: Sequence[BaseMiddleware | Callable] = (),
): ):
self.top_container: flet.Container self.top_container: flet.Container
self.center_container: flet.Container self.center_container: flet.Container
@@ -33,7 +31,7 @@ class AuthView(BaseView):
self.username: str | None = None self.username: str | None = None
self.password: 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): async def clear(self):
self.top_container.content = None self.top_container.content = None
@@ -81,7 +79,6 @@ class AuthView(BaseView):
label = flet.Text("Выберите сервер") label = flet.Text("Выберите сервер")
self.top_container.content = label self.top_container.content = label
backend_controls = flet.Column()
backend_dropdown = flet.Dropdown( backend_dropdown = flet.Dropdown(
label="Бэкенд", label="Бэкенд",
options=[ options=[
@@ -91,14 +88,7 @@ class AuthView(BaseView):
value=None if self.backend is None else self.backend.value, value=None if self.backend is None else self.backend.value,
on_change=self.callback_change_backend, on_change=self.callback_change_backend,
) )
self.center_container.content = backend_dropdown
container = flet.Container(
content=flet.Column(
controls=[backend_dropdown, backend_controls],
width=300,
),
)
self.center_container.content = container
connect_button = flet.ElevatedButton( connect_button = flet.ElevatedButton(
"Выбрать", "Выбрать",

View File

@@ -1,6 +1,6 @@
import asyncio
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from functools import partial, reduce from typing import Any, Callable
from typing import Any, Callable, Sequence
import flet import flet
from flet_route import Basket, Params 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.local import LocalBackend
from soul_diary.ui.app.backend.soul import SoulBackend from soul_diary.ui.app.backend.soul import SoulBackend
from soul_diary.ui.app.local_storage import LocalStorage 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 from soul_diary.ui.app.models import BackendType
@@ -29,21 +28,6 @@ def view(initial: bool = False, disabled: bool = False):
return decorator 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): class MetaView(type):
def __init__(cls, name: str, bases: tuple[type], attrs: dict[str, Any]): def __init__(cls, name: str, bases: tuple[type], attrs: dict[str, Any]):
super().__init__(name, bases, attrs) super().__init__(name, bases, attrs)
@@ -76,28 +60,23 @@ class BaseView(metaclass=MetaView):
is_abstract = True is_abstract = True
_initial_view: Callable | None _initial_view: Callable | None
def __init__( def __init__(self, local_storage: LocalStorage):
self,
local_storage: LocalStorage,
middlewares: Sequence[BaseMiddleware | Callable] = (),
):
self.local_storage = local_storage self.local_storage = local_storage
self.middlewares = middlewares
self.container: flet.Container self.container: flet.Container
self.stack: flet.Stack
self.view: flet.View self.view: flet.View
@setup_middlewares
async def entrypoint(self, page: flet.Page, params: Params, basket: Basket) -> flet.View: async def entrypoint(self, page: flet.Page, params: Params, basket: Basket) -> flet.View:
self.container = flet.Container() self.container = flet.Container()
self.stack = flet.Stack(controls=[self.container]) self.view = flet.View(controls=[self.container])
self.view = flet.View(controls=[self.stack], route="/test")
await self.setup() await self.setup()
await self.clear() await self.clear()
self.clear_data() 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 return self.view
async def setup(self): async def setup(self):
@@ -111,21 +90,20 @@ class BaseView(metaclass=MetaView):
@asynccontextmanager @asynccontextmanager
async def in_progress(self, page: flet.Page, tooltip: str | None = None): async def in_progress(self, page: flet.Page, tooltip: str | None = None):
for control in self.stack.controls: self.container.disabled = True
control.disabled = True page.splash = flet.Column(
self.stack.controls.append( controls=[flet.Container(
flet.Container(
content=flet.ProgressRing(tooltip=tooltip), content=flet.ProgressRing(tooltip=tooltip),
alignment=flet.alignment.center, alignment=flet.alignment.center,
), )],
alignment=flet.MainAxisAlignment.CENTER,
) )
await page.update_async() await page.update_async()
yield yield
self.stack.controls.pop() self.container.disabled = False
for control in self.stack.controls: page.splash = None
control.disabled = False
await page.update_async() await page.update_async()
async def run_initial_view(self, page: flet.Page): async def run_initial_view(self, page: flet.Page):

View File

@@ -1,10 +1,8 @@
from functools import partial from functools import partial
from typing import Awaitable, Callable, Sequence
import flet import flet
from soul_diary.ui.app.local_storage import LocalStorage 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.models import Emotion
from soul_diary.ui.app.routes import SENSE_ADD, SENSE_LIST from soul_diary.ui.app.routes import SENSE_ADD, SENSE_LIST
from .base import BaseView, view from .base import BaseView, view
@@ -14,7 +12,6 @@ class SenseAddView(BaseView):
def __init__( def __init__(
self, self,
local_storage: LocalStorage, local_storage: LocalStorage,
middlewares: Sequence[BaseMiddleware | Callable[[flet.Page], Awaitable]] = (),
): ):
self.title: flet.Text self.title: flet.Text
self.content_container: flet.Container self.content_container: flet.Container
@@ -25,7 +22,7 @@ class SenseAddView(BaseView):
self.body: str | None = None self.body: str | None = None
self.desires: 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): async def clear(self):
self.title.value = "" self.title.value = ""

View File

@@ -1,12 +1,10 @@
import asyncio import asyncio
from typing import Awaitable, Callable, Sequence
import flet import flet
from soul_diary.ui.app.backend.exceptions import NonAuthenticatedException from soul_diary.ui.app.backend.exceptions import NonAuthenticatedException
from soul_diary.ui.app.local_storage import LocalStorage 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 Sense
from soul_diary.ui.app.models import BackendType, Sense
from soul_diary.ui.app.routes import AUTH, SENSE_ADD, SENSE_LIST from soul_diary.ui.app.routes import AUTH, SENSE_ADD, SENSE_LIST
from .base import BaseView, view from .base import BaseView, view
@@ -15,13 +13,12 @@ class SenseListView(BaseView):
def __init__( def __init__(
self, self,
local_storage: LocalStorage, local_storage: LocalStorage,
middlewares: Sequence[BaseMiddleware | Callable[[flet.Page], Awaitable]] = (),
): ):
self.cards: flet.Column self.cards: flet.Column
self.local_storage = local_storage self.local_storage = local_storage
super().__init__(local_storage=local_storage, middlewares=middlewares) super().__init__(local_storage=local_storage)
async def setup(self): async def setup(self):
self.cards = flet.Column(alignment=flet.alignment.center, width=400) 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): async def callback_logout(self, event: flet.ControlEvent):
backend_client = await self.get_backend_client() 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) await event.page.go_async(AUTH)

View File

@@ -25,8 +25,9 @@ class WebService(ServiceMixin):
async def start(self): async def start(self):
app = flet_fastapi.app(SoulDiaryApp( app = flet_fastapi.app(SoulDiaryApp(
backend=BackendType.SOUL, # backend=BackendType.SOUL,
backend_data=self._backend_data, # backend_data=self._backend_data,
# backend=BackendType.LOCAL,
).run) ).run)
config = uvicorn.Config(app=app, host="0.0.0.0", port=self._port) config = uvicorn.Config(app=app, host="0.0.0.0", port=self._port)
server = UvicornServer(config) server = UvicornServer(config)