diff --git a/soul_diary/ui/app/app.py b/soul_diary/ui/app/app.py index 01aa87d..a17e2fb 100644 --- a/soul_diary/ui/app/app.py +++ b/soul_diary/ui/app/app.py @@ -22,34 +22,37 @@ class SoulDiaryApp: self._backend = backend self._backend_data = backend_data - def get_routes(self, page: flet.Page) -> dict[str, BaseView]: - local_storage = LocalStorage(client_storage=page.client_storage) - sense_list_view = SenseListView(local_storage=local_storage) + def get_routes(self) -> dict[str, BaseView]: + sense_list_view = SenseListView() return { INDEX: sense_list_view, AUTH: AuthView( - local_storage=local_storage, backend=self._backend, backend_data=self._backend_data, ), SENSE_LIST: sense_list_view, - SENSE_ADD: SenseAddView(local_storage=local_storage), + SENSE_ADD: SenseAddView(), } async def run(self, page: flet.Page): page.title = "Soul Diary" page.app = self + page.on_disconect = self.callback_disconnect - routes = self.get_routes(page) + routes = self.get_routes() Routing( page=page, async_is=True, app_routes=[ - path(url=url, clear=False, view=view.entrypoint) + path(url=url, clear=False, view=view) for url, view in routes.items() ], middleware=middleware, ) return await page.go_async(page.route) + + async def callback_disconnect(self, event: flet.ControlEvent): + local_storage = LocalStorage(event.page.client_storage) + await local_storage.clear_shared_data() diff --git a/soul_diary/ui/app/backend/base.py b/soul_diary/ui/app/backend/base.py index 71496aa..fa0c77d 100644 --- a/soul_diary/ui/app/backend/base.py +++ b/soul_diary/ui/app/backend/base.py @@ -104,7 +104,7 @@ class BaseBackend: self._token = None self._encryption_key = None self._username = None - await self._local_storage.remove_auth_data() + await self._local_storage.clear_auth_data() @property def is_auth(self) -> bool: diff --git a/soul_diary/ui/app/backend/utils.py b/soul_diary/ui/app/backend/utils.py new file mode 100644 index 0000000..b5d9d63 --- /dev/null +++ b/soul_diary/ui/app/backend/utils.py @@ -0,0 +1,30 @@ +from soul_diary.ui.app.local_storage import LocalStorage +from soul_diary.ui.app.models import BackendType +from .base import BaseBackend +from .exceptions import NonAuthenticatedException +from .local import LocalBackend +from .soul import SoulBackend + + +BACKEND_MAPPING = { + BackendType.LOCAL: LocalBackend, + BackendType.SOUL: SoulBackend, +} + + +async def get_backend_client(local_storage: LocalStorage) -> BaseBackend: + auth_data = await local_storage.get_auth_data() + if auth_data is None: + raise NonAuthenticatedException() + + backend_client_class = BACKEND_MAPPING.get(auth_data.backend, None) + if backend_client_class is None: + raise + + return backend_client_class( + local_storage=local_storage, + username=auth_data.username, + encryption_key=auth_data.encryption_key, + token=auth_data.token, + **auth_data.backend_data, + ) diff --git a/soul_diary/ui/app/controls/utils.py b/soul_diary/ui/app/controls/utils.py new file mode 100644 index 0000000..8d2717b --- /dev/null +++ b/soul_diary/ui/app/controls/utils.py @@ -0,0 +1,20 @@ +from contextlib import asynccontextmanager + +import flet + + +@asynccontextmanager +async def in_progress(page: flet.Page, tooltip: str | None = None): + 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 + + page.splash = None + await page.update_async() diff --git a/soul_diary/ui/app/local_storage.py b/soul_diary/ui/app/local_storage.py index e050210..3281d0c 100644 --- a/soul_diary/ui/app/local_storage.py +++ b/soul_diary/ui/app/local_storage.py @@ -14,6 +14,9 @@ class AuthData(BaseModel): class LocalStorage: + AUTH_DATA_KEY = "soul_diary.client.auth_data" + SHARED_DATA_KEY = "soul_diary.client.shared_data" + def __init__(self, client_storage): self._client_storage = client_storage @@ -32,21 +35,40 @@ class LocalStorage: encryption_key=encryption_key, token=token, ) - await self.raw_write("soul_diary.client", auth_data.model_dump(mode="json")) + await self.raw_write(self.AUTH_DATA_KEY, auth_data.model_dump(mode="json")) async def get_auth_data(self) -> AuthData | None: - if not await self.raw_contains("soul_diary.client"): + if not await self.raw_contains(self.AUTH_DATA_KEY): return None - data = await self.raw_read("soul_diary.client") + data = await self.raw_read(self.AUTH_DATA_KEY) return AuthData.model_validate(data) - async def remove_auth_data(self): - if await self.raw_contains("soul_diary.client"): - await self.raw_remove("soul_diary.client") + async def clear_auth_data(self): + if await self.raw_contains(self.AUTH_DATA_KEY): + await self.raw_remove(self.AUTH_DATA_KEY) - async def clear(self): - await self._client_storage.clear_async() + async def add_shared_data(self, **kwargs): + if await self.raw_contains(self.SHARED_DATA_KEY): + tmp_data = await self.raw_read(self.SHARED_DATA_KEY) + else: + tmp_data = {} + + tmp_data.update(kwargs) + await self.raw_write(self.SHARED_DATA_KEY, tmp_data) + + async def get_shared_data(self, key: str): + if not await self.raw_contains(self.SHARED_DATA_KEY): + return None + + tmp_data = await self.raw_read(self.SHARED_DATA_KEY) + return tmp_data.get(key) + + async def clear_shared_data(self): + if not await self.raw_contains(self.SHARED_DATA_KEY): + return + + await self.raw_remove(self.SHARED_DATA_KEY) async def raw_contains(self, key: str) -> bool: return await self._client_storage.contains_key_async(key) diff --git a/soul_diary/ui/app/pages/auth/choose_backend.py b/soul_diary/ui/app/pages/auth/backend.py similarity index 53% rename from soul_diary/ui/app/pages/auth/choose_backend.py rename to soul_diary/ui/app/pages/auth/backend.py index 0ce0826..6faea86 100644 --- a/soul_diary/ui/app/pages/auth/choose_backend.py +++ b/soul_diary/ui/app/pages/auth/backend.py @@ -2,20 +2,27 @@ from functools import partial import flet +from soul_diary.ui.app.local_storage import LocalStorage from soul_diary.ui.app.models import BackendType -from soul_diary.ui.app.pages.base import BasePage +from soul_diary.ui.app.pages.base import BasePage, callback_error_handle -class ChooseBackendPage(BasePage): +class BackendPage(BasePage): BACKENDS = { BackendType.LOCAL: "Локально", BackendType.SOUL: "Soul Diary сервер", } - def __init__(self, backend: BackendType | None = None): - self.backend: BackendType | None = backend + def __init__( + self, + view: flet.View, + local_storage: LocalStorage, + backend: BackendType | None = None, + ): + self.local_storage = local_storage + self.backend = backend - super().__init__() + super().__init__(view=view) def build(self) -> flet.Container: label = flet.Container( @@ -46,16 +53,40 @@ class ChooseBackendPage(BasePage): alignment=flet.alignment.center, ) + @callback_error_handle 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() + @callback_error_handle 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: + if self.backend is None or self.backend not in BackendType: dropdown.error_text = "Выберите тип бекенда" await self.update_async() + return + + await self.local_storage.add_shared_data(backend=self.backend.value) + + if self.backend == BackendType.LOCAL: + from .login import LoginPage + + await LoginPage( + view=self.view, + local_storage=self.local_storage, + backend=self.backend, + backend_registration_enabled=True, + username=await self.local_storage.get_shared_data(key="username"), + password=await self.local_storage.get_shared_data(key="password"), + ).apply() + elif self.backend == BackendType.SOUL: + from .soul_server import SoulServerPage + + backend_data = await self.local_storage.get_shared_data(key="backend_data") + if backend_data is None: + backend_data = {} + await SoulServerPage( + view=self.view, + local_storage=self.local_storage, + url=backend_data.get("url"), + ).apply() diff --git a/soul_diary/ui/app/pages/auth/login.py b/soul_diary/ui/app/pages/auth/login.py new file mode 100644 index 0000000..1e7442f --- /dev/null +++ b/soul_diary/ui/app/pages/auth/login.py @@ -0,0 +1,203 @@ +from functools import partial +from typing import Any + +import flet + +from soul_diary.ui.app.backend.exceptions import ( + IncorrectCredentialsException, + UserAlreadyExistsException, +) +from soul_diary.ui.app.backend.utils import BACKEND_MAPPING +from soul_diary.ui.app.controls.utils import in_progress +from soul_diary.ui.app.local_storage import LocalStorage +from soul_diary.ui.app.models import BackendType +from soul_diary.ui.app.pages.base import BasePage, callback_error_handle +from soul_diary.ui.app.routes import SENSE_LIST + + +class LoginPage(BasePage): + def __init__( + self, + view: flet.View, + local_storage: LocalStorage, + backend: BackendType, + backend_registration_enabled: bool, + backend_data: dict[str, Any] | None = None, + username: str | None = None, + password: str | None = None, + can_return_back: bool = True, + ): + self.local_storage = local_storage + self.backend = backend + self.backend_registration_enabled = backend_registration_enabled + self.backend_data = backend_data + self.username = username + self.password = password + self.can_return_back = can_return_back + + super().__init__(view=view) + + def build(self) -> flet.Container: + controls = [] + if self.can_return_back: + return_back_button = flet.IconButton( + icon=flet.icons.ARROW_BACK, + on_click=self.callback_return_back, + ) + controls.append(return_back_button) + top_row = flet.Row( + controls=controls, + width=300, + alignment=flet.MainAxisAlignment.START, + ) + + username_field = flet.TextField( + label="Логин", + value=self.username, + on_change=self.callback_change_username, + ) + password_field = flet.TextField( + label="Пароль", + password=True, + can_reveal_password=True, + on_change=self.callback_change_password, + ) + signin_button = flet.ElevatedButton( + text="Войти", + width=300, + height=50, + on_click=partial( + self.callback_signin, + username_field=username_field, + password_field=password_field, + ), + ) + signup_button = flet.ElevatedButton( + text="Зарегистрироваться", + width=300, + height=50, + disabled=not self.backend_registration_enabled, + on_click=partial( + self.callback_signup, + username_field=username_field, + password_field=password_field, + ), + ) + return flet.Container( + content=flet.Column( + controls=[top_row, username_field, password_field, signin_button, signup_button], + width=300, + ), + alignment=flet.alignment.center, + ) + + @callback_error_handle + async def callback_return_back(self, event: flet.ControlEvent): + await self.local_storage.add_shared_data(username=self.username) + + if self.backend == BackendType.LOCAL: + from .backend import BackendPage + + backend = await self.local_storage.get_shared_data(key="backend") + if backend is not None: + backend = BackendType(backend) + await BackendPage( + view=self.view, + local_storage=self.local_storage, + backend=backend, + ).apply() + elif self.backend == BackendType.SOUL: + from .soul_server import SoulServerPage + + backend_data = await self.local_storage.get_shared_data("backend_data") + if backend_data is None: + backend_data = {} + await SoulServerPage( + view=self.view, + local_storage=self.local_storage, + url=backend_data.get("url"), + ).apply() + + @callback_error_handle + async def callback_change_username(self, event: flet.ControlEvent): + self.username = event.control.value + + @callback_error_handle + async def callback_change_password(self, event: flet.ControlEvent): + self.password = event.control.value + + @callback_error_handle + async def callback_signup( + self, + event: flet.ControlEvent, + username_field: flet.TextField, + password_field: flet.TextField, + ): + if not self.username: + username_field.error_text = "Заполните имя пользователя" + await username_field.update_async() + if not self.password: + password_field.error_text = "Заполните пароль" + await password_field.update_async() + if not self.username or not self.password: + return + + backend_client_class = BACKEND_MAPPING[self.backend] + backend_data = await self.local_storage.get_shared_data("backend_data") + if backend_data is None: + backend_data = self.backend_data or {} + backend_client = backend_client_class( + local_storage=self.local_storage, + **backend_data, + ) + + async with in_progress(page=event.page): + try: + await backend_client.registration(username=self.username, password=self.password) + except UserAlreadyExistsException: + username_field.error_text = "Пользователь с таким именем уже существует" + password_field.error_text = None + await username_field.update_async() + await password_field.update_async() + return + + await self.local_storage.clear_shared_data() + await event.page.go_async(SENSE_LIST) + + @callback_error_handle + async def callback_signin( + self, + event: flet.ControlEvent, + username_field: flet.TextField, + password_field: flet.TextField, + ): + if not self.username: + username_field.error_text = "Заполните имя пользователя" + await username_field.update_async() + if not self.password: + password_field.error_text = "Заполните пароль" + await password_field.update_async() + if not self.username or not self.password: + return + + backend_client_class = BACKEND_MAPPING[self.backend] + backend_data = await self.local_storage.get_shared_data("backend_data") + if backend_data is None: + backend_data = self.backend_data or {} + backend_client = backend_client_class( + local_storage=self.local_storage, + **backend_data, + ) + + async with in_progress(page=event.page): + try: + await backend_client.login(username=self.username, password=self.password) + except IncorrectCredentialsException: + username_field.error_text = None + password_field.error_text = "Неверные имя пользователя и пароль" + await username_field.update_async() + await password_field.update_async() + return + + await self.local_storage.clear_shared_data() + await event.page.go_async(SENSE_LIST) diff --git a/soul_diary/ui/app/pages/auth/soul_backend_data.py b/soul_diary/ui/app/pages/auth/soul_backend_data.py deleted file mode 100644 index 88b6589..0000000 --- a/soul_diary/ui/app/pages/auth/soul_backend_data.py +++ /dev/null @@ -1,8 +0,0 @@ -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/auth/soul_server.py b/soul_diary/ui/app/pages/auth/soul_server.py new file mode 100644 index 0000000..2a8b60b --- /dev/null +++ b/soul_diary/ui/app/pages/auth/soul_server.py @@ -0,0 +1,114 @@ +from functools import partial + +import flet +from pydantic import AnyHttpUrl + +from soul_diary.ui.app.backend.soul import SoulBackend +from soul_diary.ui.app.controls.utils import in_progress +from soul_diary.ui.app.local_storage import LocalStorage +from soul_diary.ui.app.models import BackendType +from soul_diary.ui.app.pages.base import BasePage, callback_error_handle + + +class SoulServerPage(BasePage): + def __init__(self, view: flet.View, local_storage: LocalStorage, url: str | None = None): + self.local_storage = local_storage + self.url = url + + super().__init__(view=view) + + def build(self) -> flet.Container: + label = flet.Text("Soul Diary сервер") + backend_button = flet.IconButton( + icon=flet.icons.ARROW_BACK, + on_click=self.callback_return_back, + ) + top_row = flet.Row( + controls=[backend_button, label], + alignment=flet.MainAxisAlignment.START, + ) + + url_field = flet.TextField( + width=300, + label="URL", + value=self.url, + on_change=self.callback_change_url, + ) + connect_button = flet.ElevatedButton( + "Подключиться", + width=300, + height=50, + on_click=partial(self.callback_connect, url_field=url_field), + ) + + return flet.Container( + content=flet.Column( + controls=[top_row, url_field, connect_button], + width=300, + ), + alignment=flet.alignment.center, + ) + + @callback_error_handle + async def callback_change_url(self, event: flet.ControlEvent): + try: + AnyHttpUrl(event.control.value or "") + except: + event.control.error_text = "Некорректный URL" + self.url = None + else: + event.control.error_text = None + self.url = event.control.value + await event.control.update_async() + + @callback_error_handle + async def callback_return_back(self, event: flet.ControlEvent): + try: + backend_url = AnyHttpUrl(self.url) + except ValueError: + pass + else: + await self.local_storage.add_shared_data(backend_data={"url": str(backend_url)}) + + backend = await self.local_storage.get_shared_data("backend") + if backend is not None: + backend = BackendType(backend) + + from .backend import BackendPage + await BackendPage( + view=self.view, + local_storage=self.local_storage, + backend=backend, + ).apply() + + @callback_error_handle + async def callback_connect(self, event: flet.ControlEvent, url_field: flet.TextField): + try: + backend_url = AnyHttpUrl(self.url) + except ValueError: + url_field.error_text = "Некорректный URL" + await url_field.update_async() + return + + backend_client = SoulBackend(local_storage=self.local_storage, url=str(backend_url)) + async with in_progress(page=event.page): + try: + options = await backend_client.get_options() + except: + url_field.error_text = "Невозможно подключиться к серверу" + await url_field.update_async() + return + + await self.local_storage.add_shared_data(backend_data={"url": str(backend_url)}) + username = await self.local_storage.get_shared_data(key="username") + password = await self.local_storage.get_shared_data(key="password") + + from .login import LoginPage + await LoginPage( + view=self.view, + local_storage=self.local_storage, + backend=BackendType.SOUL, + backend_registration_enabled=options.registration_enabled, + username=username, + password=password, + ).apply() diff --git a/soul_diary/ui/app/pages/base.py b/soul_diary/ui/app/pages/base.py index 3914172..c6eddd0 100644 --- a/soul_diary/ui/app/pages/base.py +++ b/soul_diary/ui/app/pages/base.py @@ -1,7 +1,40 @@ +from typing import Callable + import flet class BasePage(flet.UserControl): - async def apply(self, page: flet.Page): - await page.clean_async() - await page.add_async(self) + def __init__(self, view: flet.View): + self.view = view + + super().__init__() + + async def apply(self): + self.view.controls = [self] + await self.view.update_async() + + +def callback_error_handle(function: Callable) -> Callable: + async def wrapper(self, event: flet.ControlEvent, *args, **kwargs): + async def close_dialog(event: flet.ControlEvent): + dialog.open = False + await dialog.update_async() + + try: + await function(self, event, *args, **kwargs) + except: + text = flet.Text("Ошибка") + close_button = flet.IconButton(icon=flet.icons.CLOSE, on_click=close_dialog) + dialog = flet.AlertDialog( + modal=True, + open=True, + title=flet.Row( + controls=[text, close_button], + alignment=flet.MainAxisAlignment.SPACE_BETWEEN, + ), + content=flet.Text("Произошла ошибка"), + ) + event.page.dialog = dialog + await event.page.update_async() + + return wrapper diff --git a/soul_diary/ui/app/pages/sense_add/body.py b/soul_diary/ui/app/pages/sense_add/body.py new file mode 100644 index 0000000..e5c1041 --- /dev/null +++ b/soul_diary/ui/app/pages/sense_add/body.py @@ -0,0 +1,95 @@ +from functools import partial + +import flet + +from soul_diary.ui.app.local_storage import LocalStorage +from soul_diary.ui.app.pages.base import BasePage, callback_error_handle +from soul_diary.ui.app.routes import SENSE_LIST + + +class BodyPage(BasePage): + def __init__( + self, + view: flet.View, + local_storage: LocalStorage, + body: str | None = None, + ): + self.local_storage = local_storage + self.body = body + + super().__init__(view=view) + + def build(self) -> flet.Container: + title = flet.Text("Опиши свои телесные ощущения") + close_button = flet.IconButton(icon=flet.icons.CLOSE, on_click=self.callback_close) + top_row = flet.Row( + controls=[title, close_button], + alignment=flet.MainAxisAlignment.SPACE_BETWEEN, + ) + + body_field = flet.TextField( + value=self.body, + multiline=True, + min_lines=10, + max_lines=10, + on_change=self.callback_change_body, + ) + + previous_button = flet.IconButton( + flet.icons.ARROW_BACK, + on_click=self.callback_previous, + ) + next_button = flet.IconButton( + flet.icons.ARROW_FORWARD, + on_click=partial(self.callback_next, body_field=body_field), + ) + bottom_row = flet.Row( + controls=[previous_button, next_button], + alignment=flet.MainAxisAlignment.SPACE_BETWEEN, + ) + + return flet.Container( + content=flet.Column( + controls=[top_row, body_field, bottom_row], + width=600, + ), + alignment=flet.alignment.center, + ) + + @callback_error_handle + async def callback_close(self, event: flet.ControlEvent): + await self.local_storage.clear_shared_data() + await event.page.go_async(SENSE_LIST) + + @callback_error_handle + async def callback_change_body(self, event: flet.ControlEvent): + self.body = event.control.value + + @callback_error_handle + async def callback_previous(self, event: flet.ControlEvent): + await self.local_storage.add_shared_data(body=self.body) + feelings = await self.local_storage.get_shared_data("feelings") + + from .feelings import FeelingsPage + await FeelingsPage( + view=self.view, + local_storage=self.local_storage, + feelings=feelings, + ).apply() + + @callback_error_handle + async def callback_next(self, event: flet.ControlEvent, body_field: flet.TextField): + if self.body is None or not self.body.strip(): + body_field.error_text = "Коротко опиши свои телесные ощущения" + await body_field.update_async() + return + + await self.local_storage.add_shared_data(body=self.body) + desires = await self.local_storage.get_shared_data("desires") + + from .desires import DesiresPage + await DesiresPage( + view=self.view, + local_storage=self.local_storage, + desires=desires, + ).apply() diff --git a/soul_diary/ui/app/pages/sense_add/desires.py b/soul_diary/ui/app/pages/sense_add/desires.py new file mode 100644 index 0000000..7f0f83e --- /dev/null +++ b/soul_diary/ui/app/pages/sense_add/desires.py @@ -0,0 +1,102 @@ +from functools import partial + +import flet +from soul_diary.ui.app.backend.utils import get_backend_client + +from soul_diary.ui.app.local_storage import LocalStorage +from soul_diary.ui.app.models import Emotion +from soul_diary.ui.app.pages.base import BasePage, callback_error_handle +from soul_diary.ui.app.routes import SENSE_LIST + + +class DesiresPage(BasePage): + def __init__( + self, + view: flet.View, + local_storage: LocalStorage, + desires: str | None = None, + ): + self.local_storage = local_storage + self.desires = desires + + super().__init__(view=view) + + def build(self) -> flet.Container: + title = flet.Text("Опиши свои желания на данный момент") + close_button = flet.IconButton(icon=flet.icons.CLOSE, on_click=self.callback_close) + top_row = flet.Row( + controls=[title, close_button], + alignment=flet.MainAxisAlignment.SPACE_BETWEEN, + ) + + desires_field = flet.TextField( + value=self.desires, + multiline=True, + min_lines=10, + max_lines=10, + on_change=self.callback_change_desires, + ) + + previous_button = flet.IconButton( + flet.icons.ARROW_BACK, + on_click=self.callback_previous, + ) + add_button = flet.IconButton( + flet.icons.CREATE, + on_click=partial(self.callback_add_sense, desires_field=desires_field), + ) + bottom_row = flet.Row( + controls=[previous_button, add_button], + alignment=flet.MainAxisAlignment.SPACE_BETWEEN, + ) + + return flet.Container( + content=flet.Column( + controls=[top_row, desires_field, bottom_row], + width=600, + ), + alignment=flet.alignment.center, + ) + + @callback_error_handle + async def callback_close(self, event: flet.ControlEvent): + await self.local_storage.clear_shared_data() + await event.page.go_async(SENSE_LIST) + + @callback_error_handle + async def callback_change_desires(self, event: flet.ControlEvent): + self.desires = event.control.value + + @callback_error_handle + async def callback_previous(self, event: flet.ControlEvent): + await self.local_storage.add_shared_data(desires=self.desires) + body = await self.local_storage.get_shared_data("body") + + from .body import BodyPage + await BodyPage( + view=self.view, + local_storage=self.local_storage, + body=body, + ).apply() + + @callback_error_handle + async def callback_add_sense(self, event: flet.ControlEvent, desires_field: flet.TextField): + if self.desires is None or not self.desires.strip(): + desires_field.error_text = "Коротко опиши свои желания" + await desires_field.update_async() + return + + emotions = await self.local_storage.get_shared_data("emotions") or [] + emotions = [Emotion(emotion) for emotion in emotions] + feelings = await self.local_storage.get_shared_data("feelings") + body = await self.local_storage.get_shared_data("body") + backend_client = await get_backend_client(local_storage=self.local_storage) + await backend_client.create_sense( + emotions=emotions, + feelings=feelings, + body=body, + desires=self.desires, + ) + + await self.local_storage.clear_shared_data() + await event.page.go_async(SENSE_LIST) diff --git a/soul_diary/ui/app/pages/sense_add/emotions.py b/soul_diary/ui/app/pages/sense_add/emotions.py new file mode 100644 index 0000000..ebcbe22 --- /dev/null +++ b/soul_diary/ui/app/pages/sense_add/emotions.py @@ -0,0 +1,105 @@ +from functools import partial + +import flet + +from soul_diary.ui.app.local_storage import LocalStorage +from soul_diary.ui.app.models import Emotion +from soul_diary.ui.app.pages.base import BasePage, callback_error_handle +from soul_diary.ui.app.routes import SENSE_LIST + + +class EmotionsPage(BasePage): + def __init__( + self, + view: flet.View, + local_storage: LocalStorage, + emotions: list[Emotion] | None = None, + ): + self.local_storage = local_storage + self.emotions = emotions or [] + + super().__init__(view=view) + + def build(self) -> flet.Container: + title = flet.Text("Что ты чувствуешь?") + close_button = flet.IconButton(icon=flet.icons.CLOSE, on_click=self.callback_close) + top_row = flet.Row( + controls=[title, close_button], + alignment=flet.MainAxisAlignment.SPACE_BETWEEN, + ) + + error_text = flet.Text( + "Выберите как минимум одну эмоцию", + color=flet.colors.RED, + visible=False, + ) + emotions_chips = flet.Row( + controls=[ + flet.Chip( + label=flet.Text(emotion.value), + show_checkmark=False, + selected=emotion.value in self.emotions, + on_select=partial( + self.callback_choose_emotion, + emotion=emotion, + error_text=error_text, + ), + ) + for emotion in Emotion + ], + wrap=True, + ) + emotions = flet.Column(controls=[emotions_chips, error_text]) + + next_button = flet.IconButton( + flet.icons.ARROW_FORWARD, + on_click=partial(self.callback_next, error_text=error_text), + ) + bottom_row = flet.Row( + controls=[next_button], + alignment=flet.MainAxisAlignment.END, + ) + + return flet.Container( + content=flet.Column( + controls=[top_row, emotions, bottom_row], + width=600, + ), + alignment=flet.alignment.center, + ) + + @callback_error_handle + async def callback_close(self, event: flet.ControlEvent): + await self.local_storage.clear_shared_data() + await event.page.go_async(SENSE_LIST) + + @callback_error_handle + async def callback_choose_emotion( + self, + event: flet.ControlEvent, + emotion: Emotion, + error_text: flet.Text, + ): + if event.control.selected: + self.emotions.append(emotion) + if error_text.visible: + error_text.visible = False + await error_text.update_async() + else: + self.emotions.remove(emotion) + + @callback_error_handle + async def callback_next(self, event: flet.ControlEvent, error_text: flet.Text): + if not self.emotions: + error_text.visible = True + await self.update_async() + return + await self.local_storage.add_shared_data(emotions=self.emotions) + + from .feelings import FeelingsPage + + await FeelingsPage( + view=self.view, + local_storage=self.local_storage, + feelings=await self.local_storage.get_shared_data("feelings"), + ).apply() diff --git a/soul_diary/ui/app/pages/sense_add/feelings.py b/soul_diary/ui/app/pages/sense_add/feelings.py new file mode 100644 index 0000000..5de377b --- /dev/null +++ b/soul_diary/ui/app/pages/sense_add/feelings.py @@ -0,0 +1,95 @@ +from functools import partial + +import flet + +from soul_diary.ui.app.local_storage import LocalStorage +from soul_diary.ui.app.pages.base import BasePage, callback_error_handle +from soul_diary.ui.app.routes import SENSE_LIST + + +class FeelingsPage(BasePage): + def __init__( + self, + view: flet.View, + local_storage: LocalStorage, + feelings: str | None = None, + ): + self.local_storage = local_storage + self.feelings = feelings + + super().__init__(view=view) + + def build(self) -> flet.Container: + title = flet.Text("Опиши свои чувства") + close_button = flet.IconButton(icon=flet.icons.CLOSE, on_click=self.callback_close) + top_row = flet.Row( + controls=[title, close_button], + alignment=flet.MainAxisAlignment.SPACE_BETWEEN, + ) + + feelings_field = flet.TextField( + value=self.feelings, + multiline=True, + min_lines=10, + max_lines=10, + on_change=self.callback_change_feelings, + ) + + previous_button = flet.IconButton( + flet.icons.ARROW_BACK, + on_click=self.callback_previous, + ) + next_button = flet.IconButton( + flet.icons.ARROW_FORWARD, + on_click=partial(self.callback_next, feelings_field=feelings_field), + ) + bottom_row = flet.Row( + controls=[previous_button, next_button], + alignment=flet.MainAxisAlignment.SPACE_BETWEEN, + ) + + return flet.Container( + content=flet.Column( + controls=[top_row, feelings_field, bottom_row], + width=600, + ), + alignment=flet.alignment.center, + ) + + @callback_error_handle + async def callback_close(self, event: flet.ControlEvent): + await self.local_storage.clear_shared_data() + await event.page.go_async(SENSE_LIST) + + @callback_error_handle + async def callback_change_feelings(self, event: flet.ControlEvent): + self.feelings = event.control.value + + @callback_error_handle + async def callback_previous(self, event: flet.ControlEvent): + await self.local_storage.add_shared_data(feelings=self.feelings) + emotions = await self.local_storage.get_shared_data("emotions") + + from .emotions import EmotionsPage + await EmotionsPage( + view=self.view, + local_storage=self.local_storage, + emotions=emotions, + ).apply() + + @callback_error_handle + async def callback_next(self, event: flet.ControlEvent, feelings_field: flet.TextField): + if self.feelings is None or not self.feelings.strip(): + feelings_field.error_text = "Коротко опиши свои чувства" + await feelings_field.update_async() + return + + await self.local_storage.add_shared_data(feelings=self.feelings) + body = await self.local_storage.get_shared_data("body") + + from .body import BodyPage + await BodyPage( + view=self.view, + local_storage=self.local_storage, + body=body, + ).apply() diff --git a/soul_diary/ui/app/pages/sense_list.py b/soul_diary/ui/app/pages/sense_list.py new file mode 100644 index 0000000..bd3ec7d --- /dev/null +++ b/soul_diary/ui/app/pages/sense_list.py @@ -0,0 +1,84 @@ +import flet + +from soul_diary.ui.app.backend.utils import get_backend_client +from soul_diary.ui.app.controls.utils import in_progress +from soul_diary.ui.app.local_storage import LocalStorage +from soul_diary.ui.app.models import Sense +from soul_diary.ui.app.routes import AUTH, SENSE_ADD +from .base import BasePage, callback_error_handle + + +class SenseListPage(BasePage): + def __init__(self, view: flet.View, local_storage: LocalStorage): + self.local_storage = local_storage + self.senses_cards: flet.Column + + super().__init__(view=view) + + def build(self) -> flet.Container: + self.view.vertical_alignment = flet.MainAxisAlignment.START + + add_button = flet.IconButton( + icon=flet.icons.ADD_CIRCLE_OUTLINE, + on_click=self.callback_add_sense, + ) + settings_button = flet.IconButton( + icon=flet.icons.SETTINGS, + ) + logout_button = flet.IconButton( + icon=flet.icons.LOGOUT, + on_click=self.callback_logout, + ) + top_container = flet.Container( + content=flet.Row( + controls=[add_button, settings_button, logout_button], + alignment=flet.MainAxisAlignment.END, + ), + ) + + self.senses_cards = flet.Column(alignment=flet.alignment.center) + + return flet.Container( + content=flet.Column( + controls=[top_container, self.senses_cards], + width=400, + ), + alignment=flet.alignment.center, + ) + + async def did_mount_async(self): + await self.render_cards() + + async def render_cards(self): + backend_client = await get_backend_client(self.local_storage) + senses = await backend_client.get_sense_list() + self.senses_cards.controls = [ + await self.render_card(sense) + for sense in senses + ] + await self.update_async() + + async def render_card(self, sense: Sense) -> flet.Card: + feelings = flet.Container(content=flet.Text(sense.feelings), expand=True) + created_datetime = flet.Text(sense.created_at.strftime("%d %b %H:%M")) + + return flet.Card( + content=flet.Container( + content=flet.Column(controls=[feelings, created_datetime]), + padding=10, + ), + width=400, + height=100, + ) + + @callback_error_handle + async def callback_add_sense(self, event: flet.ControlEvent): + await event.page.go_async(SENSE_ADD) + + @callback_error_handle + async def callback_logout(self, event: flet.ControlEvent): + backend_client = await get_backend_client(local_storage=self.local_storage) + async with in_progress(page=event.page): + await backend_client.logout() + await self.local_storage.clear_shared_data() + await event.page.go_async(AUTH) diff --git a/soul_diary/ui/app/views/auth.py b/soul_diary/ui/app/views/auth.py index 01da409..ad8e87f 100644 --- a/soul_diary/ui/app/views/auth.py +++ b/soul_diary/ui/app/views/auth.py @@ -1,323 +1,70 @@ import asyncio -from functools import partial from typing import Any import flet -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.controls.utils import in_progress from soul_diary.ui.app.local_storage import LocalStorage -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 -from .base import BaseView, view +from soul_diary.ui.app.models import BackendType +from soul_diary.ui.app.pages.auth.backend import BackendPage +from soul_diary.ui.app.pages.auth.login import LoginPage +from soul_diary.ui.app.pages.auth.soul_server import SoulServerPage +from soul_diary.ui.app.pages.base import BasePage +from .base import BaseView class AuthView(BaseView): def __init__( self, - local_storage: LocalStorage, backend: BackendType | None = None, backend_data: dict[str, Any] | None = None, ): - self.top_container: flet.Container - self.center_container: flet.Container - self.bottom_container: flet.Container + self.backend = backend + self.backend_data = backend_data - self.initial_backend = self.backend = backend - self.initial_backend_data = self.backend_data = backend_data or {} - self.backend_registration_enabled: bool = True - self.username: str | None = None - self.password: str | None = None - - super().__init__(local_storage=local_storage) - - async def clear(self): - self.top_container.content = None - self.center_container.content = None - self.bottom_container.content = None - - def clear_data(self): - self.backend = self.initial_backend - self.backend_data = self.initial_backend_data - self.username = None - self.password = None - - async def setup(self): - self.top_container = flet.Container(alignment=flet.alignment.center) - self.center_container = flet.Container(alignment=flet.alignment.center) - self.bottom_container = flet.Container(alignment=flet.alignment.center) - - self.container.content = flet.Column( - controls=[self.top_container, self.center_container, self.bottom_container], - width=300, - ) - self.container.alignment = flet.alignment.center - - self.view.route = AUTH - self.view.vertical_alignment = flet.MainAxisAlignment.CENTER - - @view(initial=True) - async def entrypoint_view(self, page: flet.Page): - if self.initial_backend == BackendType.SOUL: - async def connect(): - async with self.in_progress(page=page): - options = await self.connect_to_soul_server() - self.backend_registration_enabled = options.registration_enabled - await self.credentials_view(page=page) - - loop = asyncio.get_running_loop() - loop.create_task(connect()) - elif self.initial_backend == BackendType.LOCAL: - await self.credentials_view(page=page) - else: - await self.backend_view(page=page) - - @view() - async def backend_view(self, page: flet.Page): - label = flet.Text("Выберите сервер") - self.top_container.content = label - - backend_dropdown = flet.Dropdown( - label="Бэкенд", - options=[ - flet.dropdown.Option(text="Локально", key=BackendType.LOCAL.value), - flet.dropdown.Option(text="Soul Diary сервер", key=BackendType.SOUL.value), - ], - value=None if self.backend is None else self.backend.value, - on_change=self.callback_change_backend, - ) - self.center_container.content = backend_dropdown - - connect_button = flet.ElevatedButton( - "Выбрать", - width=300, - height=50, - on_click=partial(self.callback_choose_backend, dropdown=backend_dropdown), - ) - self.bottom_container.content = connect_button - - @view() - async def soul_server_data_view(self, page: flet.Page): - label = flet.Text("Soul Diary сервер") - backend_button = flet.IconButton( - icon=flet.icons.ARROW_BACK, - on_click=self.callback_go_backend, - ) - self.top_container.content = flet.Row( - controls=[backend_button, label], - width=300, - alignment=flet.MainAxisAlignment.START, - ) - - url_field = flet.TextField( - width=300, - label="URL", - value=self.backend_data.get("url"), - on_change=self.callback_change_soul_server_url, - ) - self.center_container.content = url_field - - connect_button = flet.ElevatedButton( - "Подключиться", - width=300, - height=50, - on_click=partial(self.callback_soul_server_connect, url_field=url_field), - ) - self.bottom_container.content = connect_button - - @view() - async def credentials_view(self, page: flet.Page): - controls = [] - if self.initial_backend is None: - backend_data_button = flet.IconButton( - icon=flet.icons.ARROW_BACK, - on_click=self.callback_go_backend_data, + async def entrypoint(self, page: flet.Page) -> BasePage: + local_storage = LocalStorage(client_storage=page.client_storage) + if self.backend == BackendType.SOUL: + return await self.connect_to_soul_server( + page=page, + local_storage=local_storage, ) - controls.append(backend_data_button) - self.top_container.content = flet.Row( - controls=controls, - width=300, - alignment=flet.MainAxisAlignment.START, - ) - - username_field = flet.TextField( - label="Логин", - on_change=self.callback_change_username, - ) - password_field = flet.TextField( - label="Пароль", - password=True, - can_reveal_password=True, - on_change=self.callback_change_password, - ) - self.center_container.content = flet.Column( - controls=[username_field, password_field], - width=300, - ) - - signin_button = flet.Container( - content=flet.ElevatedButton( - text="Войти", - width=300, - height=50, - on_click=partial( - self.callback_signin, - username_field=username_field, - password_field=password_field, - ), - ), - alignment=flet.alignment.center, - ) - signup_button = flet.Container( - content=flet.ElevatedButton( - text="Зарегистрироваться", - width=300, - height=50, - disabled=not self.backend_registration_enabled, - on_click=partial( - self.callback_signup, - username_field=username_field, - password_field=password_field, - ), - ), - alignment=flet.alignment.center, - ) - - self.bottom_container.content = flet.Column(controls=[signin_button, signup_button]) - - async def callback_change_backend(self, event: flet.ControlEvent): - self.backend = BackendType(event.control.value) - event.control.error_text = None - await event.page.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 event.page.update_async() - - async def callback_change_soul_server_url(self, event: flet.ControlEvent): - try: - AnyHttpUrl(event.control.value or "") - except: - event.control.error_text = "Некорректный URL" - self.backend_data["url"] = None - else: - event.control.error_text = None - self.backend_data["url"] = event.control.value - await event.page.update_async() - - async def callback_soul_server_connect( - self, - event: flet.ControlEvent, - url_field: flet.TextField, - ): - if self.backend == BackendType.SOUL: - async with self.in_progress(page=event.page): - try: - options = await self.connect_to_soul_server() - except SoulServerIncorrectURL: - url_field.error_text = "Некорректный URL" - except: - url_field.error_text = "Невозможно подключиться к серверу" - await event.page.update_async() - else: - self.backend_registration_enabled = options.registration_enabled - else: - await self.credentials_view(page=event.page) - - async def connect_to_soul_server(self) -> Options: - try: - backend_url = AnyHttpUrl(self.backend_data.get("url")) - except ValueError: - raise SoulServerIncorrectURL() - - backend_client = SoulBackend( - local_storage=self.local_storage, - url=str(backend_url), - ) - return await backend_client.get_options() - - async def callback_change_username(self, event: flet.ControlEvent): - self.username = event.control.value - - async def callback_change_password(self, event: flet.ControlEvent): - self.password = event.control.value - - async def callback_signin( - self, - event: flet.ControlEvent, - username_field: flet.TextField, - password_field: flet.TextField, - ): - if not self.username: - username_field.error_text = "Заполните имя пользователя" - if not self.password: - password_field.error_text = "Заполните пароль" - if not self.username or not self.password: - await event.page.update_async() - return - - backend_client_class = self.BACKEND_MAPPING.get(self.backend) - if backend_client_class is None: - raise - backend_client = backend_client_class( - local_storage=self.local_storage, - **self.backend_data, - ) - - async with self.in_progress(page=event.page): - try: - await backend_client.login(username=self.username, password=self.password) - except IncorrectCredentialsException: - password_field.error_text = "Неверные имя пользователя и пароль" - await event.page.update_async() - return - - await event.page.go_async(SENSE_LIST) - - async def callback_signup( - self, - event: flet.ControlEvent, - username_field: flet.TextField, - password_field: flet.TextField, - ): - if not self.username: - username_field.error_text = "Заполните имя пользователя" - if not self.password: - password_field.error_text = "Заполните пароль" - if not self.username or not self.password: - await event.page.update_async() - return - - backend_client_class = self.BACKEND_MAPPING.get(self.backend) - if backend_client_class is None: - raise - backend_client = backend_client_class( - local_storage=self.local_storage, - **self.backend_data, - ) - - async with self.in_progress(page=event.page): - try: - await backend_client.registration(username=self.username, password=self.password) - except UserAlreadyExistsException: - username_field.error_text = "Пользователь с таким именем уже существует" - await event.page.update_async() - return - - await event.page.go_async(SENSE_LIST) - - async def callback_go_backend(self, event: flet.ControlEvent): - await self.backend_view(page=event.page) - - async def callback_go_backend_data(self, event: flet.ControlEvent): - if self.backend == BackendType.SOUL: - await self.soul_server_data_view(page=event.page) elif self.backend == BackendType.LOCAL: - await self.backend_view(page=event.page) + return LoginPage( + view=self.view, + backend=self.backend, + backend_data=self.backend_data, + backend_registration_enabled=True, + local_storage=local_storage, + can_return_back=False, + ) + else: + return BackendPage(view=self.view, local_storage=local_storage) + + async def connect_to_soul_server(self, page: flet.Page, local_storage: LocalStorage) -> BasePage: + backend_data = await local_storage.get_shared_data("backend_data") + if backend_data is None: + backend_data = {} + soul_backend_client = SoulBackend( + local_storage=local_storage, + url=self.backend_data.get("url") or backend_data.get("url"), + ) + try: + async with in_progress(page=page): + options = await soul_backend_client.get_options() + except: + return SoulServerPage( + view=self.view, + local_storage=local_storage, + url=self.backend_data.get("url") or backend_data.get("url"), + ) + else: + return LoginPage( + view=self.view, + local_storage=local_storage, + backend=self.backend, + backend_data=self.backend_data, + backend_registration_enabled=options.registration_enabled, + can_return_back=False, + ) diff --git a/soul_diary/ui/app/views/base.py b/soul_diary/ui/app/views/base.py index ff9931a..63c0436 100644 --- a/soul_diary/ui/app/views/base.py +++ b/soul_diary/ui/app/views/base.py @@ -1,128 +1,17 @@ -import asyncio -from contextlib import asynccontextmanager -from typing import Any, Callable - import flet from flet_route import Basket, Params -from soul_diary.ui.app.backend.base import BaseBackend -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.models import BackendType +from soul_diary.ui.app.pages.base import BasePage -def view(initial: bool = False, disabled: bool = False): - def decorator(function: Callable): - async def wrapper(self, page: flet.Page): - await self.clear() +class BaseView: + async def __call__(self, page: flet.Page, params: Params, basket: Basket) -> flet.View: + self.view = flet.View(vertical_alignment=flet.MainAxisAlignment.CENTER) - await function(self, page=page) - await page.update_async() - - wrapper._view_enabled = not disabled - wrapper._view_initial = initial - - return wrapper - return decorator - - -class MetaView(type): - def __init__(cls, name: str, bases: tuple[type], attrs: dict[str, Any]): - super().__init__(name, bases, attrs) - - is_abstract = attrs.pop("is_abstract", False) - if not is_abstract: - cls.setup_class(attrs=attrs) - - def setup_class(cls, attrs: dict[str, Any]): - initial_view = None - for attr in attrs.values(): - view_enabled = getattr(attr, "_view_enabled", False) - view_initial = getattr(attr, "_view_initial", False) - if view_enabled and view_initial: - if initial_view is not None: - raise ValueError(f"Initial view already defined: {initial_view.__name__}") - initial_view = attr - if initial_view is None: - raise ValueError("Initial view must be defined") - - cls._initial_view = initial_view - - -class BaseView(metaclass=MetaView): - BACKEND_MAPPING = { - BackendType.LOCAL: LocalBackend, - BackendType.SOUL: SoulBackend, - } - - is_abstract = True - _initial_view: Callable | None - - def __init__(self, local_storage: LocalStorage): - self.local_storage = local_storage - - self.container: flet.Container - self.view: flet.View - - async def entrypoint(self, page: flet.Page, params: Params, basket: Basket) -> flet.View: - self.container = flet.Container() - self.view = flet.View(controls=[self.container]) - - await self.setup() - await self.clear() - self.clear_data() - - loop = asyncio.get_running_loop() - loop.create_task(self.run_initial_view(page=page)) + page = await self.entrypoint(page=page) + self.view.controls = [page] return self.view - async def setup(self): - pass - - async def clear(self): - pass - - def clear_data(self): - pass - - @asynccontextmanager - async def in_progress(self, page: flet.Page, tooltip: str | None = None): - 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.container.disabled = False - page.splash = None - await page.update_async() - - async def run_initial_view(self, page: flet.Page): - if self._initial_view is not None: - await self._initial_view(page=page) - - async def get_backend_client(self) -> BaseBackend: - auth_data = await self.local_storage.get_auth_data() - if auth_data is None: - raise NonAuthenticatedException() - - backend_client_class = self.BACKEND_MAPPING.get(auth_data.backend, None) - if backend_client_class is None: - raise - - return backend_client_class( - local_storage=self.local_storage, - username=auth_data.username, - encryption_key=auth_data.encryption_key, - token=auth_data.token, - **auth_data.backend_data, - ) + async def entrypoint(self, page: flet.Page) -> BasePage: + raise NotImplementedError diff --git a/soul_diary/ui/app/views/exceptions.py b/soul_diary/ui/app/views/exceptions.py deleted file mode 100644 index b3b1240..0000000 --- a/soul_diary/ui/app/views/exceptions.py +++ /dev/null @@ -1,2 +0,0 @@ -class SoulServerIncorrectURL(Exception): - pass diff --git a/soul_diary/ui/app/views/onboarding.py b/soul_diary/ui/app/views/onboarding.py deleted file mode 100644 index e69de29..0000000 diff --git a/soul_diary/ui/app/views/sense_add.py b/soul_diary/ui/app/views/sense_add.py index 45ca878..f8bfbf5 100644 --- a/soul_diary/ui/app/views/sense_add.py +++ b/soul_diary/ui/app/views/sense_add.py @@ -1,235 +1,12 @@ -from functools import partial - import flet - from soul_diary.ui.app.local_storage import LocalStorage -from soul_diary.ui.app.models import Emotion -from soul_diary.ui.app.routes import SENSE_ADD, SENSE_LIST -from .base import BaseView, view + +from soul_diary.ui.app.pages.base import BasePage +from soul_diary.ui.app.pages.sense_add.emotions import EmotionsPage +from .base import BaseView class SenseAddView(BaseView): - def __init__( - self, - local_storage: LocalStorage, - ): - self.title: flet.Text - self.content_container: flet.Container - self.buttons_row: flet.Row - - self.emotions: list[Emotion] = [] - self.feelings: str | None = None - self.body: str | None = None - self.desires: str | None = None - - super().__init__(local_storage=local_storage) - - async def clear(self): - self.title.value = "" - self.content_container.content = None - self.buttons_row.controls = [] - - def clear_data(self): - self.emotions = [] - self.feelings = None - self.body = None - self.desires = None - - async def setup(self): - # Top - self.title = flet.Text() - close_button = flet.IconButton(icon=flet.icons.CLOSE, on_click=self.callback_close) - top_container = flet.Container( - content=flet.Row( - [self.title, close_button], - alignment=flet.MainAxisAlignment.SPACE_BETWEEN, - ), - margin=10, - ) - - # Center - self.content_container = flet.Container() - center_container = flet.Container(content=self.content_container, margin=10) - - # Bottom - self.buttons_row = flet.Row() - bottom_container = flet.Container(content=self.buttons_row, margin=10) - - # Build - self.container.content = flet.Column( - controls=[top_container, center_container, bottom_container], - width=600, - ) - self.container.alignment = flet.alignment.center - - self.view.route = SENSE_ADD - self.view.vertical_alignment = flet.MainAxisAlignment.CENTER - - @view(initial=True) - async def emotions_view(self, page: flet.Page): - self.title.value = "Что ты чувствуешь?" - - chips = flet.Row( - controls=[ - flet.Chip( - label=flet.Text(emotion.value), - show_checkmark=False, - selected=emotion.value in self.emotions, - on_select=partial(self.callback_choose_emotion, emotion=emotion), - ) - for emotion in Emotion - ], - wrap=True, - ) - self.content_container.content = flet.Column( - controls=[chips], - ) - - next_button = flet.IconButton( - flet.icons.ARROW_FORWARD, - on_click=self.callback_go_feelings_from_emotions, - ) - self.buttons_row.controls = [next_button] - self.buttons_row.alignment = flet.MainAxisAlignment.END - - @view() - async def feelings_view(self, page: flet.Page): - self.title.value = "Опиши свои чувства" - - self.content_container.content = flet.TextField( - value=self.feelings, - multiline=True, - min_lines=10, - max_lines=10, - on_change=self.callback_change_feelings, - ) - - previous_button = flet.IconButton( - flet.icons.ARROW_BACK, - on_click=self.callback_go_emotions_from_feelings, - ) - next_button = flet.IconButton( - flet.icons.ARROW_FORWARD, - on_click=self.callback_go_body_from_feelings, - ) - self.buttons_row.controls = [previous_button, next_button] - self.buttons_row.alignment = flet.MainAxisAlignment.SPACE_BETWEEN - - @view() - async def body_view(self, page: flet.Page): - self.title.value = "Опиши свои телесные ощущения" - - self.content_container.content = flet.TextField( - value=self.body, - multiline=True, - min_lines=10, - max_lines=10, - on_change=self.callback_change_body, - ) - - previous_button = flet.IconButton( - flet.icons.ARROW_BACK, - on_click=self.callback_go_feelings_from_body, - ) - next_button = flet.IconButton( - flet.icons.ARROW_FORWARD, - on_click=self.callback_go_desires_from_body, - ) - self.buttons_row.controls = [previous_button, next_button] - self.buttons_row.alignment = flet.MainAxisAlignment.SPACE_BETWEEN - - @view() - async def desires_view(self, page: flet.Page): - self.title.value = "Опиши свои желания на данный момент" - - self.content_container.content = flet.TextField( - value=self.desires, - multiline=True, - min_lines=10, - max_lines=10, - on_change=self.callback_change_desires, - ) - - previous_button = flet.IconButton( - flet.icons.ARROW_BACK, - on_click=self.callback_go_body_from_desires, - ) - apply_button = flet.IconButton(flet.icons.CREATE, on_click=self.callback_add_sense) - self.buttons_row.controls = [previous_button, apply_button] - self.buttons_row.alignment = flet.MainAxisAlignment.SPACE_BETWEEN - - async def callback_close(self, event: flet.ControlEvent): - self.clear_data() - await event.page.go_async(SENSE_LIST) - - async def callback_choose_emotion(self, event: flet.ControlEvent, emotion: Emotion): - if event.control.selected: - self.emotions.append(emotion) - emotions_column = self.content_container.content - if len(emotions_column.controls) > 1: - emotions_column.controls = emotions_column.controls[:1] - await event.page.update_async() - else: - self.emotions.remove(emotion) - - async def callback_change_feelings(self, event: flet.ControlEvent): - self.feelings = event.control.value - - async def callback_change_body(self, event: flet.ControlEvent): - self.body = event.control.value - - async def callback_change_desires(self, event: flet.ControlEvent): - self.desires = event.control.value - - async def callback_go_emotions_from_feelings(self, event: flet.ControlEvent): - await self.emotions_view(page=event.page) - - async def callback_go_feelings_from_emotions(self, event: flet.ControlEvent): - if not self.emotions: - emotions_column = self.content_container.content - error_text = flet.Text("Выберите как минимум одну эмоцию", color=flet.colors.RED) - emotions_column.controls = [emotions_column.controls[0], error_text] - await event.page.update_async() - return - - await self.feelings_view(page=event.page) - - async def callback_go_feelings_from_body(self, event: flet.ControlEvent): - await self.feelings_view(page=event.page) - - async def callback_go_body_from_feelings(self, event: flet.ControlEvent): - if self.feelings is None or not self.feelings.strip(): - self.content_container.content.error_text = "Коротко опиши свои чувства" - await event.page.update_async() - return - - await self.body_view(page=event.page) - - async def callback_go_body_from_desires(self, event: flet.ControlEvent): - await self.body_view(page=event.page) - - async def callback_go_desires_from_body(self, event: flet.ControlEvent): - if self.body is None or not self.body.strip(): - self.content_container.content.error_text = "Коротко опиши свои телесные ощущения" - await event.page.update_async() - return - - await self.desires_view(page=event.page) - - async def callback_add_sense(self, event: flet.ControlEvent): - if self.desires is None or not self.desires.strip(): - self.content_container.content.error_text = "Коротко опиши свои желания" - await event.page.update_async() - return - - backend_client = await self.get_backend_client() - async with self.in_progress(page=event.page): - await backend_client.create_sense( - emotions=self.emotions, - feelings=self.feelings, - body=self.body, - desires=self.desires, - ) - - self.clear_data() - await event.page.go_async(SENSE_LIST) + async def entrypoint(self, page: flet.Page) -> BasePage: + local_storage = LocalStorage(page.client_storage) + return EmotionsPage(view=self.view, local_storage=local_storage) diff --git a/soul_diary/ui/app/views/sense_list.py b/soul_diary/ui/app/views/sense_list.py index 99d4266..a1c0329 100644 --- a/soul_diary/ui/app/views/sense_list.py +++ b/soul_diary/ui/app/views/sense_list.py @@ -1,99 +1,12 @@ -import asyncio - 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.models import Sense -from soul_diary.ui.app.routes import AUTH, SENSE_ADD, SENSE_LIST -from .base import BaseView, view +from soul_diary.ui.app.pages.base import BasePage +from soul_diary.ui.app.pages.sense_list import SenseListPage +from .base import BaseView class SenseListView(BaseView): - def __init__( - self, - local_storage: LocalStorage, - ): - self.cards: flet.Column - - self.local_storage = local_storage - - super().__init__(local_storage=local_storage) - - async def setup(self): - self.cards = flet.Column(alignment=flet.alignment.center, width=400) - - add_button = flet.IconButton( - icon=flet.icons.ADD_CIRCLE_OUTLINE, - on_click=self.callback_add_sense, - ) - settings_button = flet.IconButton( - icon=flet.icons.SETTINGS, - ) - logout_button = flet.IconButton( - icon=flet.icons.LOGOUT, - on_click=self.callback_logout, - ) - top_container = flet.Container( - content=flet.Row( - controls=[add_button, settings_button, logout_button], - alignment=flet.MainAxisAlignment.END, - ), - ) - - self.container.content = flet.Column( - controls=[top_container, self.cards], - width=400, - ) - self.container.alignment = flet.alignment.center - - self.view.route = SENSE_LIST - self.view.vertical_alignment = flet.MainAxisAlignment.CENTER - self.view.scroll = flet.ScrollMode.ALWAYS - - async def clear(self): - self.cards.controls = [] - - @view(initial=True) - async def sense_list_view(self, page: flet.Page): - self.cards.controls = [ - flet.Container( - content=flet.ProgressRing(), - alignment=flet.alignment.center, - ), - ] - - loop = asyncio.get_running_loop() - loop.create_task(self.render_sense_list(page=page)) - - async def render_sense_list(self, page: flet.Page): - auth_data = await self.local_storage.get_auth_data() - if auth_data is None: - raise NonAuthenticatedException() - - backend_client = await self.get_backend_client() - senses = await backend_client.get_sense_list() - self.cards.controls = [await self.render_card_from_sense(sense) for sense in senses] - await page.update_async() - - async def render_card_from_sense(self, sense: Sense) -> flet.Card: - feelings = flet.Container(content=flet.Text(sense.feelings), expand=True) - created_datetime = flet.Text(sense.created_at.strftime("%d %b %H:%M")) - - return flet.Card( - content=flet.Container( - content=flet.Column(controls=[feelings, created_datetime]), - padding=10, - ), - width=400, - height=100, - ) - - async def callback_add_sense(self, event: flet.ControlEvent): - await event.page.go_async(SENSE_ADD) - - async def callback_logout(self, event: flet.ControlEvent): - backend_client = await self.get_backend_client() - async with self.in_progress(page=event.page): - await backend_client.logout() - await event.page.go_async(AUTH) + async def entrypoint(self, page: flet.Page) -> BasePage: + local_storage = LocalStorage(client_storage=page.client_storage) + return SenseListPage(view=self.view, local_storage=local_storage) diff --git a/soul_diary/ui/web/service.py b/soul_diary/ui/web/service.py index e8bd407..77b5efc 100644 --- a/soul_diary/ui/web/service.py +++ b/soul_diary/ui/web/service.py @@ -25,9 +25,8 @@ class WebService(ServiceMixin): async def start(self): app = flet_fastapi.app(SoulDiaryApp( - # backend=BackendType.SOUL, - # backend_data=self._backend_data, - # backend=BackendType.LOCAL, + backend=BackendType.SOUL, + backend_data=self._backend_data, ).run) config = uvicorn.Config(app=app, host="0.0.0.0", port=self._port) server = UvicornServer(config)