Fixed darkmode load and save

This commit is contained in:
2025-11-23 22:28:56 +01:00
parent b1be747101
commit dd9aefa143
8 changed files with 73 additions and 48 deletions

View File

@@ -10,7 +10,7 @@ from myfasthtml.controls.Keyboard import Keyboard
from myfasthtml.controls.Layout import Layout from myfasthtml.controls.Layout import Layout
from myfasthtml.controls.TabsManager import TabsManager from myfasthtml.controls.TabsManager import TabsManager
from myfasthtml.controls.helpers import Ids, mk from myfasthtml.controls.helpers import Ids, mk
from myfasthtml.core.instances import SingleInstance from myfasthtml.core.instances import UniqueInstance
from myfasthtml.icons.carbon import volume_object_storage from myfasthtml.icons.carbon import volume_object_storage
from myfasthtml.icons.fluent_p3 import folder_open20_regular from myfasthtml.icons.fluent_p3 import folder_open20_regular
from myfasthtml.myfastapp import create_app from myfasthtml.myfastapp import create_app
@@ -32,11 +32,11 @@ app, rt = create_app(protect_routes=True,
@rt("/") @rt("/")
def index(session): def index(session):
session_instance = SingleInstance(session=session, _id=Ids.UserSession) session_instance = UniqueInstance(session=session, _id=Ids.UserSession)
layout = Layout(session_instance, "Testing Layout") layout = Layout(session_instance, "Testing Layout")
layout.set_footer("Goodbye World") layout.set_footer("Goodbye World")
tabs_manager = TabsManager(layout, _id=f"{TabsManager.get_prefix()}-main") tabs_manager = TabsManager(layout, _id=f"{TabsManager.compute_prefix()}-main")
btn_show_right_drawer = mk.button("show", btn_show_right_drawer = mk.button("show",
command=layout.commands.toggle_drawer("right"), command=layout.commands.toggle_drawer("right"),
id="btn_show_right_drawer_id") id="btn_show_right_drawer_id")

View File

@@ -9,8 +9,9 @@ class InstancesDebugger(SingleInstance):
def render(self): def render(self):
instances = self._get_instances() instances = self._get_instances()
nodes, edges = from_parent_child_list(instances, nodes, edges = from_parent_child_list(
id_getter=lambda x: x.get_id(), instances,
id_getter=lambda x: f"{InstancesManager.get_session_id(x.get_session())}-{x.get_id()}",
label_getter=lambda x: x.get_prefix(), label_getter=lambda x: x.get_prefix(),
parent_getter=lambda x: x.get_parent().get_id() if x.get_parent() else None parent_getter=lambda x: x.get_parent().get_id() if x.get_parent() else None
) )
@@ -22,7 +23,7 @@ class InstancesDebugger(SingleInstance):
node["shape"] = "box" node["shape"] = "box"
vis_network = VisNetwork(self, nodes=nodes, edges=edges) vis_network = VisNetwork(self, nodes=nodes, edges=edges)
#vis_network.add_to_options(physics={"wind": {"x": 0, "y": 1}}) # vis_network.add_to_options(physics={"wind": {"x": 0, "y": 1}})
return vis_network return vis_network
def _get_instances(self): def _get_instances(self):

View File

@@ -192,7 +192,7 @@ class Layout(SingleInstance):
cls="flex gap-1" cls="flex gap-1"
), ),
Div( # right Div( # right
*self.header_right.get_content(), *self.header_right.get_content()[None],
UserProfile(self), UserProfile(self),
cls="flex gap-1" cls="flex gap-1"
), ),

View File

@@ -12,8 +12,7 @@ from myfasthtml.controls.VisNetwork import VisNetwork
from myfasthtml.controls.helpers import mk from myfasthtml.controls.helpers import mk
from myfasthtml.core.commands import Command from myfasthtml.core.commands import Command
from myfasthtml.core.dbmanager import DbObject from myfasthtml.core.dbmanager import DbObject
from myfasthtml.core.instances import MultipleInstance, BaseInstance from myfasthtml.core.instances import MultipleInstance, BaseInstance, InstancesManager
from myfasthtml.core.instances_helper import InstancesHelper
from myfasthtml.icons.fluent_p1 import tabs24_regular from myfasthtml.icons.fluent_p1 import tabs24_regular
from myfasthtml.icons.fluent_p3 import dismiss_circle16_regular, tab_add24_regular from myfasthtml.icons.fluent_p3 import dismiss_circle16_regular, tab_add24_regular
@@ -102,7 +101,7 @@ class TabsManager(MultipleInstance):
tab_config = self._state.tabs[tab_id] tab_config = self._state.tabs[tab_id]
if tab_config["component_type"] is None: if tab_config["component_type"] is None:
return None return None
return InstancesHelper.dynamic_get(self, tab_config["component_type"], tab_config["component_id"]) return InstancesManager.dynamic_get(self, tab_config["component_type"], tab_config["component_id"])
@staticmethod @staticmethod
def _get_tab_count(): def _get_tab_count():

View File

@@ -4,7 +4,7 @@ from myfasthtml.controls.BaseCommands import BaseCommands
from myfasthtml.controls.helpers import mk from myfasthtml.controls.helpers import mk
from myfasthtml.core.AuthProxy import AuthProxy from myfasthtml.core.AuthProxy import AuthProxy
from myfasthtml.core.commands import Command from myfasthtml.core.commands import Command
from myfasthtml.core.instances import SingleInstance, InstancesManager, RootInstance from myfasthtml.core.instances import SingleInstance, RootInstance
from myfasthtml.core.utils import retrieve_user_info from myfasthtml.core.utils import retrieve_user_info
from myfasthtml.icons.material import dark_mode_filled, person_outline_sharp from myfasthtml.icons.material import dark_mode_filled, person_outline_sharp
from myfasthtml.icons.material_p1 import light_mode_filled, alternate_email_filled from myfasthtml.icons.material_p1 import light_mode_filled, alternate_email_filled
@@ -16,6 +16,7 @@ class UserProfileState:
self._session = owner.get_session() self._session = owner.get_session()
self.theme = "light" self.theme = "light"
self.load()
def load(self): def load(self):
user_info = retrieve_user_info(self._session) user_info = retrieve_user_info(self._session)
@@ -44,6 +45,7 @@ class UserProfile(SingleInstance):
def update_dark_mode(self, client_response): def update_dark_mode(self, client_response):
self._state.theme = client_response.get("theme", "light") self._state.theme = client_response.get("theme", "light")
self._state.save() self._state.save()
retrieve_user_info(self._session).get("user_settings", {})["theme"] = self._state.theme
def render(self): def render(self):
user_info = retrieve_user_info(self._session) user_info = retrieve_user_info(self._session)

View File

@@ -1,8 +1,13 @@
import logging
import uuid import uuid
from typing import Optional from typing import Optional
from dbengine.utils import get_class
from myfasthtml.controls.helpers import Ids from myfasthtml.controls.helpers import Ids
from myfasthtml.core.utils import pascal_to_snake from myfasthtml.core.utils import pascal_to_snake, snake_to_pascal
logger = logging.getLogger("InstancesManager")
special_session = { special_session = {
"user_info": {"id": "** SPECIAL SESSION **"} "user_info": {"id": "** SPECIAL SESSION **"}
@@ -57,13 +62,14 @@ class BaseInstance:
if not getattr(self, "_is_new_instance", False): if not getattr(self, "_is_new_instance", False):
# Skip __init__ if instance already existed # Skip __init__ if instance already existed
return return
else: elif not isinstance(self, UniqueInstance):
# make sure that it's no longer considered as a new instance # No more __init__ unless it's UniqueInstance
self._is_new_instance = False self._is_new_instance = False
self._parent = parent self._parent = parent
self._session = session or (parent.get_session() if parent else None) self._session = session or (parent.get_session() if parent else None)
self._id = _id or self.compute_id() self._id = _id or self.compute_id()
self._prefix = self._id if isinstance(self, (UniqueInstance, SingleInstance)) else self.compute_prefix()
if auto_register: if auto_register:
InstancesManager.register(self._session, self) InstancesManager.register(self._session, self)
@@ -77,13 +83,16 @@ class BaseInstance:
def get_parent(self) -> Optional['BaseInstance']: def get_parent(self) -> Optional['BaseInstance']:
return self._parent return self._parent
def get_prefix(self) -> str:
return self._prefix
@classmethod @classmethod
def get_prefix(cls): def compute_prefix(cls):
return f"mf-{pascal_to_snake(cls.__name__)}" return f"mf-{pascal_to_snake(cls.__name__)}"
@classmethod @classmethod
def compute_id(cls): def compute_id(cls):
prefix = cls.get_prefix() prefix = cls.compute_prefix()
if issubclass(cls, SingleInstance): if issubclass(cls, SingleInstance):
_id = prefix _id = prefix
else: else:
@@ -104,6 +113,20 @@ class SingleInstance(BaseInstance):
super().__init__(parent, session, _id, auto_register) super().__init__(parent, session, _id, auto_register)
class UniqueInstance(BaseInstance):
"""
Base class for instances that can only have one instance at a time.
But unlike SingleInstance, the __init__ is called every time it's instantiated.
"""
def __init__(self,
parent: Optional[BaseInstance] = None,
session: Optional[dict] = None,
_id: Optional[str] = None,
auto_register: bool = True):
super().__init__(parent, session, _id, auto_register)
class MultipleInstance(BaseInstance): class MultipleInstance(BaseInstance):
""" """
Base class for instances that can have multiple instances at a time. Base class for instances that can have multiple instances at a time.
@@ -158,5 +181,19 @@ class InstancesManager:
def reset(): def reset():
InstancesManager.instances.clear() InstancesManager.instances.clear()
@staticmethod
def dynamic_get(parent: BaseInstance, component_type: str, instance_id: str):
logger.debug(f"Dynamic get: {component_type=} {instance_id=}")
cls = InstancesManager._get_class_name(component_type)
fully_qualified_name = f"myfasthtml.controls.{cls}.{cls}"
cls = get_class(fully_qualified_name)
return cls(parent, instance_id)
@staticmethod
def _get_class_name(component_type: str) -> str:
component_type = component_type.replace("mf-", "")
component_type = snake_to_pascal(component_type)
return component_type
RootInstance = SingleInstance(None, special_session, Ids.Root) RootInstance = SingleInstance(None, special_session, Ids.Root)

View File

@@ -1,29 +0,0 @@
import logging
from myfasthtml.controls.CommandsDebugger import CommandsDebugger
from myfasthtml.controls.FileUpload import FileUpload
from myfasthtml.controls.InstancesDebugger import InstancesDebugger
from myfasthtml.controls.VisNetwork import VisNetwork
from myfasthtml.controls.helpers import Ids
from myfasthtml.core.instances import BaseInstance, InstancesManager
logger = logging.getLogger("InstancesHelper")
class InstancesHelper:
@staticmethod
def dynamic_get(parent: BaseInstance, component_type: str, instance_id: str):
logger.debug(f"Dynamic get: {component_type} {instance_id}")
if component_type == Ids.VisNetwork:
return InstancesManager.get(parent.get_session(), instance_id,
VisNetwork, parent=parent, _id=instance_id)
elif component_type == Ids.InstancesDebugger:
return InstancesManager.get(parent.get_session(), instance_id,
InstancesDebugger, parent.get_session(), parent, instance_id)
elif component_type == Ids.CommandsDebugger:
return InstancesManager.get(parent.get_session(), instance_id,
CommandsDebugger, parent.get_session(), parent, instance_id)
elif component_type == Ids.FileUpload:
return InstancesManager.get(parent.get_session(), instance_id, FileUpload, parent)
logger.warning(f"Unknown component type: {component_type}")
return None

View File

@@ -247,6 +247,21 @@ def pascal_to_snake(name: str) -> str:
s2 = re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', s1) s2 = re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', s1)
return s2.lower() return s2.lower()
def snake_to_pascal(name: str) -> str:
"""Convert a snake_case string to PascalCase."""
if name is None:
return None
name = name.strip()
if not name:
return ""
# Split on underscores and capitalize each part
parts = name.split('_')
return ''.join(word.capitalize() for word in parts if word)
@utils_rt(Routes.Commands) @utils_rt(Routes.Commands)
def post(session, c_id: str, client_response: dict = None): def post(session, c_id: str, client_response: dict = None):
""" """