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.TabsManager import TabsManager
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.fluent_p3 import folder_open20_regular
from myfasthtml.myfastapp import create_app
@@ -32,11 +32,11 @@ app, rt = create_app(protect_routes=True,
@rt("/")
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.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",
command=layout.commands.toggle_drawer("right"),
id="btn_show_right_drawer_id")

View File

@@ -9,11 +9,12 @@ class InstancesDebugger(SingleInstance):
def render(self):
instances = self._get_instances()
nodes, edges = from_parent_child_list(instances,
id_getter=lambda x: x.get_id(),
label_getter=lambda x: x.get_prefix(),
parent_getter=lambda x: x.get_parent().get_id() if x.get_parent() else None
)
nodes, edges = from_parent_child_list(
instances,
id_getter=lambda x: f"{InstancesManager.get_session_id(x.get_session())}-{x.get_id()}",
label_getter=lambda x: x.get_prefix(),
parent_getter=lambda x: x.get_parent().get_id() if x.get_parent() else None
)
for edge in edges:
edge["color"] = "green"
edge["arrows"] = {"to": {"enabled": False, "type": "circle"}}
@@ -22,7 +23,7 @@ class InstancesDebugger(SingleInstance):
node["shape"] = "box"
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
def _get_instances(self):

View File

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

View File

@@ -12,8 +12,7 @@ from myfasthtml.controls.VisNetwork import VisNetwork
from myfasthtml.controls.helpers import mk
from myfasthtml.core.commands import Command
from myfasthtml.core.dbmanager import DbObject
from myfasthtml.core.instances import MultipleInstance, BaseInstance
from myfasthtml.core.instances_helper import InstancesHelper
from myfasthtml.core.instances import MultipleInstance, BaseInstance, InstancesManager
from myfasthtml.icons.fluent_p1 import tabs24_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]
if tab_config["component_type"] is 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
def _get_tab_count():

View File

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

View File

@@ -1,8 +1,13 @@
import logging
import uuid
from typing import Optional
from dbengine.utils import get_class
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 = {
"user_info": {"id": "** SPECIAL SESSION **"}
@@ -57,13 +62,14 @@ class BaseInstance:
if not getattr(self, "_is_new_instance", False):
# Skip __init__ if instance already existed
return
else:
# make sure that it's no longer considered as a new instance
elif not isinstance(self, UniqueInstance):
# No more __init__ unless it's UniqueInstance
self._is_new_instance = False
self._parent = parent
self._session = session or (parent.get_session() if parent else None)
self._id = _id or self.compute_id()
self._prefix = self._id if isinstance(self, (UniqueInstance, SingleInstance)) else self.compute_prefix()
if auto_register:
InstancesManager.register(self._session, self)
@@ -77,13 +83,16 @@ class BaseInstance:
def get_parent(self) -> Optional['BaseInstance']:
return self._parent
def get_prefix(self) -> str:
return self._prefix
@classmethod
def get_prefix(cls):
def compute_prefix(cls):
return f"mf-{pascal_to_snake(cls.__name__)}"
@classmethod
def compute_id(cls):
prefix = cls.get_prefix()
prefix = cls.compute_prefix()
if issubclass(cls, SingleInstance):
_id = prefix
else:
@@ -104,6 +113,20 @@ class SingleInstance(BaseInstance):
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):
"""
Base class for instances that can have multiple instances at a time.
@@ -157,6 +180,20 @@ class InstancesManager:
@staticmethod
def reset():
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)

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)
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)
def post(session, c_id: str, client_response: dict = None):
"""