diff --git a/README.md b/README.md index a794e74..30326e8 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/soul_diary/backend/api/settings.py b/soul_diary/backend/api/settings.py index bdc0ce8..c7dacd3 100644 --- a/soul_diary/backend/api/settings.py +++ b/soul_diary/backend/api/settings.py @@ -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 diff --git a/soul_diary/backend/database/settings.py b/soul_diary/backend/database/settings.py index f6391d8..5228a6c 100644 --- a/soul_diary/backend/database/settings.py +++ b/soul_diary/backend/database/settings.py @@ -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" diff --git a/soul_diary/ui/app/middleware.py b/soul_diary/ui/app/middleware.py index 8bcee65..8cad5e3 100644 --- a/soul_diary/ui/app/middleware.py +++ b/soul_diary/ui/app/middleware.py @@ -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 diff --git a/soul_diary/ui/app/middlewares/__init__.py b/soul_diary/ui/app/middlewares/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/soul_diary/ui/app/middlewares/base.py b/soul_diary/ui/app/middlewares/base.py deleted file mode 100644 index a67834c..0000000 --- a/soul_diary/ui/app/middlewares/base.py +++ /dev/null @@ -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 diff --git a/soul_diary/ui/app/pages/auth/choose_backend.py b/soul_diary/ui/app/pages/auth/choose_backend.py new file mode 100644 index 0000000..0ce0826 --- /dev/null +++ b/soul_diary/ui/app/pages/auth/choose_backend.py @@ -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() diff --git a/soul_diary/ui/app/pages/auth/soul_backend_data.py b/soul_diary/ui/app/pages/auth/soul_backend_data.py new file mode 100644 index 0000000..88b6589 --- /dev/null +++ b/soul_diary/ui/app/pages/auth/soul_backend_data.py @@ -0,0 +1,8 @@ +import flet + +from soul_diary.ui.app.pages.base import BasePage + + +class SoulBackendDataPage(BasePage): + def build(self) -> flet.Container: + pass diff --git a/soul_diary/ui/app/pages/base.py b/soul_diary/ui/app/pages/base.py new file mode 100644 index 0000000..3914172 --- /dev/null +++ b/soul_diary/ui/app/pages/base.py @@ -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) diff --git a/soul_diary/ui/app/views/auth.py b/soul_diary/ui/app/views/auth.py index a1f5615..01da409 100644 --- a/soul_diary/ui/app/views/auth.py +++ b/soul_diary/ui/app/views/auth.py @@ -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( "Выбрать", diff --git a/soul_diary/ui/app/views/base.py b/soul_diary/ui/app/views/base.py index ebf0467..ff9931a 100644 --- a/soul_diary/ui/app/views/base.py +++ b/soul_diary/ui/app/views/base.py @@ -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): diff --git a/soul_diary/ui/app/views/sense_add.py b/soul_diary/ui/app/views/sense_add.py index 0932786..45ca878 100644 --- a/soul_diary/ui/app/views/sense_add.py +++ b/soul_diary/ui/app/views/sense_add.py @@ -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 = "" diff --git a/soul_diary/ui/app/views/sense_list.py b/soul_diary/ui/app/views/sense_list.py index db8ce53..99d4266 100644 --- a/soul_diary/ui/app/views/sense_list.py +++ b/soul_diary/ui/app/views/sense_list.py @@ -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) diff --git a/soul_diary/ui/web/service.py b/soul_diary/ui/web/service.py index 77b5efc..e8bd407 100644 --- a/soul_diary/ui/web/service.py +++ b/soul_diary/ui/web/service.py @@ -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)