Refactoring
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
30
soul_diary/ui/app/backend/utils.py
Normal file
30
soul_diary/ui/app/backend/utils.py
Normal 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,
|
||||||
|
)
|
||||||
20
soul_diary/ui/app/controls/utils.py
Normal file
20
soul_diary/ui/app/controls/utils.py
Normal 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()
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
203
soul_diary/ui/app/pages/auth/login.py
Normal file
203
soul_diary/ui/app/pages/auth/login.py
Normal 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)
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import flet
|
|
||||||
|
|
||||||
from soul_diary.ui.app.pages.base import BasePage
|
|
||||||
|
|
||||||
|
|
||||||
class SoulBackendDataPage(BasePage):
|
|
||||||
def build(self) -> flet.Container:
|
|
||||||
pass
|
|
||||||
114
soul_diary/ui/app/pages/auth/soul_server.py
Normal file
114
soul_diary/ui/app/pages/auth/soul_server.py
Normal 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()
|
||||||
@@ -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
|
||||||
|
|||||||
95
soul_diary/ui/app/pages/sense_add/body.py
Normal file
95
soul_diary/ui/app/pages/sense_add/body.py
Normal 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()
|
||||||
102
soul_diary/ui/app/pages/sense_add/desires.py
Normal file
102
soul_diary/ui/app/pages/sense_add/desires.py
Normal 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)
|
||||||
105
soul_diary/ui/app/pages/sense_add/emotions.py
Normal file
105
soul_diary/ui/app/pages/sense_add/emotions.py
Normal 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()
|
||||||
95
soul_diary/ui/app/pages/sense_add/feelings.py
Normal file
95
soul_diary/ui/app/pages/sense_add/feelings.py
Normal 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()
|
||||||
84
soul_diary/ui/app/pages/sense_list.py
Normal file
84
soul_diary/ui/app/pages/sense_list.py
Normal 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)
|
||||||
@@ -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
|
if self.backend == BackendType.SOUL:
|
||||||
self.username: str | None = None
|
return await self.connect_to_soul_server(
|
||||||
self.password: str | None = None
|
page=page,
|
||||||
|
local_storage=local_storage,
|
||||||
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:
|
|
||||||
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:
|
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,
|
||||||
|
)
|
||||||
|
|||||||
@@ -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,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
class SoulServerIncorrectURL(Exception):
|
|
||||||
pass
|
|
||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user