Refactoring

This commit is contained in:
2023-12-16 09:29:28 +03:00
parent 7f4aeb921e
commit 5b369718c2
22 changed files with 1041 additions and 789 deletions

View File

@@ -22,34 +22,37 @@ class SoulDiaryApp:
self._backend = backend self._backend = backend
self._backend_data = backend_data self._backend_data = backend_data
def get_routes(self, page: flet.Page) -> dict[str, BaseView]: def get_routes(self) -> dict[str, BaseView]:
local_storage = LocalStorage(client_storage=page.client_storage) sense_list_view = SenseListView()
sense_list_view = SenseListView(local_storage=local_storage)
return { return {
INDEX: sense_list_view, INDEX: sense_list_view,
AUTH: AuthView( AUTH: AuthView(
local_storage=local_storage,
backend=self._backend, backend=self._backend,
backend_data=self._backend_data, backend_data=self._backend_data,
), ),
SENSE_LIST: sense_list_view, SENSE_LIST: sense_list_view,
SENSE_ADD: SenseAddView(local_storage=local_storage), SENSE_ADD: SenseAddView(),
} }
async def run(self, page: flet.Page): async def run(self, page: flet.Page):
page.title = "Soul Diary" page.title = "Soul Diary"
page.app = self page.app = self
page.on_disconect = self.callback_disconnect
routes = self.get_routes(page) routes = self.get_routes()
Routing( Routing(
page=page, page=page,
async_is=True, async_is=True,
app_routes=[ app_routes=[
path(url=url, clear=False, view=view.entrypoint) path(url=url, clear=False, view=view)
for url, view in routes.items() for url, view in routes.items()
], ],
middleware=middleware, middleware=middleware,
) )
return await page.go_async(page.route) 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()

View File

@@ -104,7 +104,7 @@ class BaseBackend:
self._token = None self._token = None
self._encryption_key = None self._encryption_key = None
self._username = None self._username = None
await self._local_storage.remove_auth_data() await self._local_storage.clear_auth_data()
@property @property
def is_auth(self) -> bool: def is_auth(self) -> bool:

View File

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

View File

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

View File

@@ -14,6 +14,9 @@ class AuthData(BaseModel):
class LocalStorage: class LocalStorage:
AUTH_DATA_KEY = "soul_diary.client.auth_data"
SHARED_DATA_KEY = "soul_diary.client.shared_data"
def __init__(self, client_storage): def __init__(self, client_storage):
self._client_storage = client_storage self._client_storage = client_storage
@@ -32,21 +35,40 @@ class LocalStorage:
encryption_key=encryption_key, encryption_key=encryption_key,
token=token, 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: 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 return None
data = await self.raw_read("soul_diary.client") data = await self.raw_read(self.AUTH_DATA_KEY)
return AuthData.model_validate(data) return AuthData.model_validate(data)
async def remove_auth_data(self): async def clear_auth_data(self):
if await self.raw_contains("soul_diary.client"): if await self.raw_contains(self.AUTH_DATA_KEY):
await self.raw_remove("soul_diary.client") await self.raw_remove(self.AUTH_DATA_KEY)
async def clear(self): async def add_shared_data(self, **kwargs):
await self._client_storage.clear_async() 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: async def raw_contains(self, key: str) -> bool:
return await self._client_storage.contains_key_async(key) return await self._client_storage.contains_key_async(key)

View File

@@ -2,20 +2,27 @@ from functools import partial
import flet import flet
from soul_diary.ui.app.local_storage import LocalStorage
from soul_diary.ui.app.models import BackendType 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 = { BACKENDS = {
BackendType.LOCAL: "Локально", BackendType.LOCAL: "Локально",
BackendType.SOUL: "Soul Diary сервер", BackendType.SOUL: "Soul Diary сервер",
} }
def __init__(self, backend: BackendType | None = None): def __init__(
self.backend: BackendType | None = backend 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: def build(self) -> flet.Container:
label = flet.Container( label = flet.Container(
@@ -46,16 +53,40 @@ class ChooseBackendPage(BasePage):
alignment=flet.alignment.center, alignment=flet.alignment.center,
) )
@callback_error_handle
async def callback_change_backend(self, event: flet.ControlEvent): async def callback_change_backend(self, event: flet.ControlEvent):
self.backend = BackendType(event.control.value) self.backend = BackendType(event.control.value)
event.control.error_text = None event.control.error_text = None
await event.control.update_async() await event.control.update_async()
@callback_error_handle
async def callback_choose_backend(self, event: flet.ControlEvent, dropdown: flet.Dropdown): async def callback_choose_backend(self, event: flet.ControlEvent, dropdown: flet.Dropdown):
if self.backend == BackendType.LOCAL: if self.backend is None or self.backend not in BackendType:
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 = "Выберите тип бекенда" dropdown.error_text = "Выберите тип бекенда"
await self.update_async() 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()

View File

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

View File

@@ -1,8 +0,0 @@
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,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()

View File

@@ -1,7 +1,40 @@
from typing import Callable
import flet import flet
class BasePage(flet.UserControl): class BasePage(flet.UserControl):
async def apply(self, page: flet.Page): def __init__(self, view: flet.View):
await page.clean_async() self.view = view
await page.add_async(self)
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,323 +1,70 @@
import asyncio import asyncio
from functools import partial
from typing import Any from typing import Any
import flet 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.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.local_storage import LocalStorage
from soul_diary.ui.app.models import BackendType, Options from soul_diary.ui.app.models import BackendType
from soul_diary.ui.app.routes import AUTH, SENSE_LIST from soul_diary.ui.app.pages.auth.backend import BackendPage
from soul_diary.ui.app.views.exceptions import SoulServerIncorrectURL from soul_diary.ui.app.pages.auth.login import LoginPage
from .base import BaseView, view 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): class AuthView(BaseView):
def __init__( def __init__(
self, self,
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,
): ):
self.top_container: flet.Container self.backend = backend
self.center_container: flet.Container self.backend_data = backend_data
self.bottom_container: flet.Container
self.initial_backend = self.backend = backend async def entrypoint(self, page: flet.Page) -> BasePage:
self.initial_backend_data = self.backend_data = backend_data or {} local_storage = LocalStorage(client_storage=page.client_storage)
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,
)
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: if self.backend == BackendType.SOUL:
async with self.in_progress(page=event.page): return await self.connect_to_soul_server(
try: page=page,
options = await self.connect_to_soul_server() local_storage=local_storage,
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: 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,
)

View File

@@ -1,128 +1,17 @@
import asyncio
from contextlib import asynccontextmanager
from typing import Any, Callable
import flet import flet
from flet_route import Basket, Params from flet_route import Basket, Params
from soul_diary.ui.app.backend.base import BaseBackend from soul_diary.ui.app.pages.base import BasePage
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
def view(initial: bool = False, disabled: bool = False): class BaseView:
def decorator(function: Callable): async def __call__(self, page: flet.Page, params: Params, basket: Basket) -> flet.View:
async def wrapper(self, page: flet.Page): self.view = flet.View(vertical_alignment=flet.MainAxisAlignment.CENTER)
await self.clear()
await function(self, page=page) page = await self.entrypoint(page=page)
await page.update_async() self.view.controls = [page]
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))
return self.view return self.view
async def setup(self): async def entrypoint(self, page: flet.Page) -> BasePage:
pass raise NotImplementedError
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,
)

View File

@@ -1,2 +0,0 @@
class SoulServerIncorrectURL(Exception):
pass

View File

@@ -1,235 +1,12 @@
from functools import partial
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.models import Emotion
from soul_diary.ui.app.routes import SENSE_ADD, SENSE_LIST from soul_diary.ui.app.pages.base import BasePage
from .base import BaseView, view from soul_diary.ui.app.pages.sense_add.emotions import EmotionsPage
from .base import BaseView
class SenseAddView(BaseView): class SenseAddView(BaseView):
def __init__( async def entrypoint(self, page: flet.Page) -> BasePage:
self, local_storage = LocalStorage(page.client_storage)
local_storage: LocalStorage, return EmotionsPage(view=self.view, local_storage=local_storage)
):
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)

View File

@@ -1,99 +1,12 @@
import asyncio
import flet 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.local_storage import LocalStorage
from soul_diary.ui.app.models import Sense from soul_diary.ui.app.pages.base import BasePage
from soul_diary.ui.app.routes import AUTH, SENSE_ADD, SENSE_LIST from soul_diary.ui.app.pages.sense_list import SenseListPage
from .base import BaseView, view from .base import BaseView
class SenseListView(BaseView): class SenseListView(BaseView):
def __init__( async def entrypoint(self, page: flet.Page) -> BasePage:
self, local_storage = LocalStorage(client_storage=page.client_storage)
local_storage: LocalStorage, return SenseListPage(view=self.view, local_storage=local_storage)
):
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)

View File

@@ -25,9 +25,8 @@ 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)