feat: enhance sidebar branding and move theme color settings

This commit is contained in:
ihmily
2026-07-12 15:54:18 +08:00
parent 589248f2c4
commit c9ef45d169
4 changed files with 372 additions and 219 deletions

View File

@@ -170,6 +170,9 @@ class App:
if page := self.pages.get(page_name):
await self.settings.is_changed()
self.current_page = page
rail = getattr(self.left_navigation_menu, "rail", None)
if rail is not None:
rail.select_page(page_name)
await page.load()
finally:
self._loading_page = False

View File

@@ -1,218 +1,221 @@
from collections.abc import Callable
from typing import cast
import flet as ft
from ..themes import PopupColorItem, ThemeManager
class ControlGroup:
def __init__(self, icon, label: str, index: int, name: str, selected_icon):
self.icon = icon
self.label = label
self.index = index
self.name = name
self.selected_icon = selected_icon
class NavigationItem(ft.Container):
def __init__(self, destination: ControlGroup, item_clicked: Callable[[ft.Event[ft.Container]], None]):
super().__init__()
self.ink = True
self.padding = 10
self.border_radius = 5
self.destination = destination
self.icon = destination.icon
self.text = destination.label
self.icon_control = ft.Icon(destination.icon, color=ft.Colors.PRIMARY)
self.label_control = ft.Text(destination.label, color=ft.Colors.PRIMARY)
self.content = ft.Row([self.icon_control, self.label_control])
self.on_click = item_clicked
class NavigationColumn(ft.Column):
def __init__(self, sidebar, page, app):
super().__init__()
self.expand = 4
self.spacing = 0
self.scroll = ft.ScrollMode.ALWAYS
self.sidebar = sidebar
self.selected_index = 0
self.flet_page = page
self.app = app
self.navigation_items = self.get_navigation_items()
self.controls = cast(list[ft.Control], self.navigation_items)
def get_navigation_items(self) -> list[NavigationItem]:
return [
NavigationItem(destination, item_clicked=self.item_clicked) for destination in self.sidebar.control_groups
]
def item_clicked(self, e: ft.Event[ft.Container]):
control = cast(NavigationItem, e.control)
self.selected_index = control.destination.index
self.update_selected_item()
self.flet_page.go(f"/{control.destination.name}")
def update_selected_item(self):
for item in self.navigation_items:
item.bgcolor = None
item.icon_control.icon = item.destination.icon
if 0 <= self.selected_index < len(self.navigation_items):
selected_item = self.navigation_items[self.selected_index]
selected_item.bgcolor = ft.Colors.SECONDARY_CONTAINER
selected_item.icon_control.icon = selected_item.destination.selected_icon
class LeftNavigationMenu(ft.Column):
def __init__(self, app):
super().__init__()
self.app = app
self.sidebar = app.sidebar
self.flet_page = app.page
self.rail = None
self.dark_light_text = None
self.dark_light_icon = None
self.bottom_controls = None
self.first_run = True
self.theme_manager = ThemeManager(self.app)
self.app.language_manager.add_observer(self)
self._ = {}
self.load()
def load(self):
self._ = self.app.language_manager.language.get("sidebar")
self.rail = NavigationColumn(sidebar=self.sidebar, page=self.flet_page, app=self.app)
if self.flet_page.theme_mode == ft.ThemeMode.DARK:
self.dark_light_text = ft.Text(self._["dark_theme"], color=ft.Colors.PRIMARY)
self.dark_light_icon = ft.IconButton(
icon=ft.Icons.BRIGHTNESS_HIGH_OUTLINED,
tooltip=self._["toggle_day_theme"],
on_click=self.theme_changed,
icon_color=ft.Colors.PRIMARY,
)
else:
self.dark_light_text = ft.Text(self._["light_theme"], color=ft.Colors.PRIMARY)
self.dark_light_icon = ft.IconButton(
icon=ft.Icons.BRIGHTNESS_2_OUTLINED,
tooltip=self._["toggle_night_theme"],
on_click=self.theme_changed,
icon_color=ft.Colors.PRIMARY,
)
colors_list = [
("deeppurple", "Deep purple"),
("purple", "Purple"),
("indigo", "Indigo"),
("blue", "Blue"),
("teal", "Teal"),
("deeporange", "Deep orange"),
("orange", "Orange"),
("pink", "Pink"),
("brown", "Brown"),
("bluegrey", "Blue Grey"),
("green", "Green"),
("cyan", "Cyan"),
("lightblue", "Light Blue"),
("", "Default"),
]
self.bottom_controls = ft.Column(
controls=[
ft.Row(
controls=[
self.dark_light_icon,
self.dark_light_text,
],
alignment=ft.MainAxisAlignment.START,
),
ft.Row(
controls=[
ft.PopupMenuButton(
icon=ft.Icons.COLOR_LENS_OUTLINED,
icon_color=ft.Colors.PRIMARY,
tooltip=self._["colors"],
items=[PopupColorItem(color=color, name=name) for color, name in colors_list],
),
ft.Text(self._["theme_color"], color=ft.Colors.PRIMARY),
],
alignment=ft.MainAxisAlignment.START,
),
],
alignment=ft.MainAxisAlignment.END,
)
self.controls = [
self.rail,
ft.Container(expand=True),
self.bottom_controls,
]
self.width = 160
self.spacing = 0
self.alignment = ft.MainAxisAlignment.START
async def theme_changed(self, _):
page = self.app.page
self._ = self.app.language_manager.language.get("sidebar")
if page.theme_mode == ft.ThemeMode.LIGHT:
page.theme_mode = ft.ThemeMode.DARK
self.dark_light_text.value = self._["dark_theme"]
self.dark_light_icon.icon = ft.Icons.BRIGHTNESS_HIGH_OUTLINED
self.dark_light_icon.tooltip = self._["toggle_day_theme"]
self.app.settings.user_config["theme_mode"] = "dark"
else:
page.theme_mode = ft.ThemeMode.LIGHT
self.dark_light_text.value = self._["light_theme"]
self.dark_light_icon.icon = ft.Icons.BRIGHTNESS_2_OUTLINED
self.dark_light_icon.tooltip = self._["toggle_night_theme"]
self.app.settings.user_config["theme_mode"] = "light"
self.app.services.run_coro(self.app.config_manager.save_user_config(self.app.settings.user_config))
await self.on_theme_change()
page.update()
async def on_theme_change(self):
"""When the theme changes, recreate the content and update the page"""
page_list = ["home", "about"]
if self.app.current_page.page_name in page_list:
await self.app.current_page.load()
class NavigationSidebar:
def __init__(self, app):
self.app = app
self.control_groups = []
self.selected_control_group = None
self.app.language_manager.add_observer(self)
self._ = {}
self.load()
def load(self):
self._ = self.app.language_manager.language.get("sidebar")
self.control_groups = [
ControlGroup(icon=ft.Icons.HOME, label=self._["home"], index=0, name="home", selected_icon=ft.Icons.HOME),
ControlGroup(
icon=ft.Icons.DASHBOARD,
label=self._["recordings"],
index=1,
name="recordings",
selected_icon=ft.Icons.DASHBOARD_ROUNDED,
),
ControlGroup(
icon=ft.Icons.SETTINGS,
label=self._["settings"],
index=2,
name="settings",
selected_icon=ft.Icons.SETTINGS,
),
ControlGroup(
icon=ft.Icons.DRIVE_FILE_MOVE,
label=self._["storage"],
index=3,
name="storage",
selected_icon=ft.Icons.DRIVE_FILE_MOVE_OUTLINE,
),
ControlGroup(icon=ft.Icons.INFO, label=self._["about"], index=4, name="about", selected_icon=ft.Icons.INFO),
]
self.selected_control_group = self.control_groups[0]
from collections.abc import Callable
from typing import cast
import flet as ft
from ..themes import ThemeManager
class ControlGroup:
def __init__(self, icon, label: str, index: int, name: str, selected_icon):
self.icon = icon
self.label = label
self.index = index
self.name = name
self.selected_icon = selected_icon
class NavigationItem(ft.Container):
def __init__(self, destination: ControlGroup, item_clicked: Callable[[ft.Event[ft.Container]], None]):
super().__init__()
self.ink = True
self.padding = ft.Padding.symmetric(horizontal=12, vertical=10)
self.margin = ft.Margin.symmetric(horizontal=8, vertical=2)
self.border_radius = 12
self.destination = destination
self.icon = destination.icon
self.text = destination.label
self.icon_control = ft.Icon(destination.icon, color=ft.Colors.PRIMARY, size=21)
self.label_control = ft.Text(destination.label, color=ft.Colors.PRIMARY, size=14)
self.content = ft.Row([self.icon_control, self.label_control], spacing=12)
self.on_click = item_clicked
class NavigationColumn(ft.Column):
def __init__(self, sidebar, page, app):
super().__init__()
self.expand = 4
self.spacing = 0
self.scroll = ft.ScrollMode.ALWAYS
self.sidebar = sidebar
self.selected_index = 0
self.flet_page = page
self.app = app
self.navigation_items = self.get_navigation_items()
self.controls = cast(list[ft.Control], self.navigation_items)
current_page = getattr(self.app, "current_page", None)
if current_page is not None:
self.select_page(current_page.page_name)
else:
self.update_selected_item()
def get_navigation_items(self) -> list[NavigationItem]:
return [
NavigationItem(destination, item_clicked=self.item_clicked) for destination in self.sidebar.control_groups
]
def item_clicked(self, e: ft.Event[ft.Container]):
control = cast(NavigationItem, e.control)
self.selected_index = control.destination.index
self.update_selected_item()
self.flet_page.go(f"/{control.destination.name}")
def update_selected_item(self):
for item in self.navigation_items:
item.bgcolor = None
item.icon_control.icon = item.destination.icon
item.icon_control.color = ft.Colors.PRIMARY
item.label_control.color = ft.Colors.PRIMARY
item.label_control.weight = None
if 0 <= self.selected_index < len(self.navigation_items):
selected_item = self.navigation_items[self.selected_index]
selected_item.bgcolor = ft.Colors.SECONDARY_CONTAINER
selected_item.icon_control.icon = selected_item.destination.selected_icon
selected_item.icon_control.color = ft.Colors.PRIMARY
selected_item.label_control.color = ft.Colors.PRIMARY
selected_item.label_control.weight = ft.FontWeight.W_600
def select_page(self, page_name: str):
for item in self.navigation_items:
if item.destination.name == page_name:
self.selected_index = item.destination.index
break
self.update_selected_item()
class LeftNavigationMenu(ft.Column):
def __init__(self, app):
super().__init__()
self.app = app
self.sidebar = app.sidebar
self.flet_page = app.page
self.rail = None
self.dark_light_text = None
self.dark_light_icon = None
self.bottom_controls = None
self.first_run = True
self.theme_manager = ThemeManager(self.app)
self.app.language_manager.add_observer(self)
self._ = {}
self.load()
def load(self):
self._ = self.app.language_manager.language.get("sidebar")
self.rail = NavigationColumn(sidebar=self.sidebar, page=self.flet_page, app=self.app)
brand_header = ft.Container(
content=ft.Row(
controls=[
ft.Image(src="images/logo-mark.svg", width=38, height=38, fit=ft.BoxFit.CONTAIN),
ft.Text("StreamCap", size=18, weight=ft.FontWeight.BOLD),
],
spacing=10,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
),
padding=ft.Padding.only(left=14, right=12, top=18, bottom=18),
)
if self.flet_page.theme_mode == ft.ThemeMode.DARK:
self.dark_light_text = ft.Text(self._["dark_theme"], color=ft.Colors.PRIMARY)
self.dark_light_icon = ft.IconButton(
icon=ft.Icons.BRIGHTNESS_HIGH_OUTLINED,
tooltip=self._["toggle_day_theme"],
on_click=self.theme_changed,
icon_color=ft.Colors.PRIMARY,
)
else:
self.dark_light_text = ft.Text(self._["light_theme"], color=ft.Colors.PRIMARY)
self.dark_light_icon = ft.IconButton(
icon=ft.Icons.BRIGHTNESS_2_OUTLINED,
tooltip=self._["toggle_night_theme"],
on_click=self.theme_changed,
icon_color=ft.Colors.PRIMARY,
)
self.bottom_controls = ft.Column(
controls=[
ft.Row(
controls=[
self.dark_light_icon,
self.dark_light_text,
],
alignment=ft.MainAxisAlignment.START,
),
],
alignment=ft.MainAxisAlignment.END,
)
self.controls = [
brand_header,
self.rail,
ft.Container(expand=True),
ft.Container(content=self.bottom_controls, padding=ft.Padding.only(left=6, bottom=12)),
]
self.width = 192
self.spacing = 0
self.alignment = ft.MainAxisAlignment.START
async def theme_changed(self, _):
page = self.app.page
self._ = self.app.language_manager.language.get("sidebar")
if page.theme_mode == ft.ThemeMode.LIGHT:
page.theme_mode = ft.ThemeMode.DARK
self.dark_light_text.value = self._["dark_theme"]
self.dark_light_icon.icon = ft.Icons.BRIGHTNESS_HIGH_OUTLINED
self.dark_light_icon.tooltip = self._["toggle_day_theme"]
self.app.settings.user_config["theme_mode"] = "dark"
else:
page.theme_mode = ft.ThemeMode.LIGHT
self.dark_light_text.value = self._["light_theme"]
self.dark_light_icon.icon = ft.Icons.BRIGHTNESS_2_OUTLINED
self.dark_light_icon.tooltip = self._["toggle_night_theme"]
self.app.settings.user_config["theme_mode"] = "light"
self.app.services.run_coro(self.app.config_manager.save_user_config(self.app.settings.user_config))
await self.on_theme_change()
page.update()
async def on_theme_change(self):
"""When the theme changes, recreate the content and update the page"""
page_list = ["home", "about"]
if self.app.current_page.page_name in page_list:
await self.app.current_page.load()
class NavigationSidebar:
def __init__(self, app):
self.app = app
self.control_groups = []
self.selected_control_group = None
self.app.language_manager.add_observer(self)
self._ = {}
self.load()
def load(self):
self._ = self.app.language_manager.language.get("sidebar")
self.control_groups = [
ControlGroup(icon=ft.Icons.HOME, label=self._["home"], index=0, name="home", selected_icon=ft.Icons.HOME),
ControlGroup(
icon=ft.Icons.DASHBOARD,
label=self._["recordings"],
index=1,
name="recordings",
selected_icon=ft.Icons.DASHBOARD_ROUNDED,
),
ControlGroup(
icon=ft.Icons.SETTINGS,
label=self._["settings"],
index=2,
name="settings",
selected_icon=ft.Icons.SETTINGS,
),
ControlGroup(
icon=ft.Icons.DRIVE_FILE_MOVE,
label=self._["storage"],
index=3,
name="storage",
selected_icon=ft.Icons.DRIVE_FILE_MOVE_OUTLINE,
),
ControlGroup(icon=ft.Icons.INFO, label=self._["about"], index=4, name="about", selected_icon=ft.Icons.INFO),
]
self.selected_control_group = self.control_groups[0]

View File

@@ -10,6 +10,7 @@ from ...utils.delay import DelayedTaskExecutor
from ...utils.logger import logger
from ..base_page import PageBase
from ..components.dialogs.help_dialog import HelpDialog
from ..themes import PopupColorItem
class SettingsPage(PageBase):
@@ -41,7 +42,7 @@ class SettingsPage(PageBase):
async def load(self):
self.content_area.controls.clear()
language = self.app.language_manager.language
self._ = language["settings_page"] | language["video_quality"] | language["base"]
self._ = language["settings_page"] | language["video_quality"] | language["base"] | language["sidebar"]
self.tab_recording = self.create_recording_settings_tab()
self.tab_push = self.create_push_settings_tab()
self.tab_cookies = self.create_cookies_settings_tab()
@@ -264,6 +265,33 @@ class SettingsPage(PageBase):
tooltip=self._["switch_language"],
),
),
self.create_setting_row(
self._["theme_color"],
ft.PopupMenuButton(
icon=ft.Icons.COLOR_LENS_OUTLINED,
icon_color=ft.Colors.PRIMARY,
tooltip=self._["colors"],
items=[
PopupColorItem(color=color, name=name)
for color, name in [
("deeppurple", "Deep purple"),
("purple", "Purple"),
("indigo", "Indigo"),
("blue", "Blue"),
("teal", "Teal"),
("deeporange", "Deep orange"),
("orange", "Orange"),
("pink", "Pink"),
("brown", "Brown"),
("bluegrey", "Blue Grey"),
("green", "Green"),
("cyan", "Cyan"),
("lightblue", "Light Blue"),
("", "Default"),
]
],
),
),
self.create_setting_row(
self._["filename_includes_title"],
ft.Switch(

119
assets/images/logo-mark.svg Normal file
View File

@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 94 100" width="38" height="38">
<title>StreamCap panda mark</title>
<g transform="translate(-358 -108)">
<path fill="#030303" d="
M 373.42 166.22
A 0.91 0.91 0 0 0 373.50 165.01
Q 370.03 160.67 369.27 155.35
C 368.21 147.99 368.81 141.11 376.40 138.15
C 378.44 137.35 380.82 137.52 383.03 137.08
A 2.41 2.41 0 0 0 384.66 135.89
Q 387.74 130.40 393.65 127.41
Q 395.16 126.64 396.67 125.87
A 1.79 1.75 83.5 0 0 397.58 124.70
C 398.04 122.73 398.55 120.58 399.53 118.97
Q 404.76 110.40 415.09 113.63
C 419.40 114.98 421.73 120.05 421.31 124.26
C 421.08 126.62 422.14 127.69 423.97 128.06
A 1.21 1.20 28.2 0 0 425.04 127.74
Q 431.08 121.86 438.89 125.24
Q 443.36 127.19 445.35 132.72
Q 448.97 142.80 441.50 149.95
A 1.20 1.18 -32.6 0 0 441.21 151.23
C 448.14 168.86 444.41 187.23 434.02 202.50
C 432.76 204.35 431.92 204.65 429.64 204.68
Q 403.48 204.93 362.93 204.59
A 0.72 0.72 0 0 1 362.22 203.91
C 361.48 189.92 363.10 176.44 373.42 166.22
Z
M 371.58 155.77
L 372.62 147.80
A 2.53 2.52 -57.2 0 0 372.26 146.13
L 371.95 145.62
A 0.54 0.53 32.9 0 0 370.96 145.83
C 370.12 153.17 370.43 158.75 375.38 164.61
A 0.95 0.95 0 0 0 376.36 164.92
C 389.48 161.49 399.28 166.74 409.68 174.40
C 415.52 178.70 423.04 178.73 429.59 176.36
C 434.70 174.50 443.31 169.04 442.66 162.64
Q 442.16 157.73 439.84 152.51
A 1.31 1.30 67.8 0 0 438.20 151.82
Q 434.93 153.03 431.25 152.47
A 0.66 0.65 -86.4 0 1 430.69 151.83
Q 430.63 144.31 423.81 142.01
A 2.52 2.50 3.9 0 1 422.15 140.12
Q 421.04 134.60 423.37 130.53
A 0.60 0.59 30.2 0 0 423.15 129.72
Q 417.97 126.79 415.74 126.27
Q 401.91 123.10 390.66 131.42
Q 387.72 133.60 386.00 137.16
A 2.53 2.50 7.8 0 1 384.16 138.54
L 376.91 139.80
A 0.91 0.91 0 0 0 376.40 140.07
L 375.71 140.76
A 0.34 0.33 20.7 0 0 375.97 141.33
C 383.49 140.92 382.01 149.42 375.59 147.46
A 0.87 0.87 0 0 0 374.52 147.98
Q 373.10 151.75 373.08 155.30
A 0.17 0.16 -16.8 0 1 373.00 155.44
L 372.07 156.07
A 0.32 0.31 -13.4 0 1 371.58 155.77
Z"/>
<path fill="#fdfdfd" d="
M 371.58 155.77
A 0.32 0.31 -13.4 0 0 372.07 156.07
L 373.00 155.44
A 0.17 0.16 -16.8 0 0 373.08 155.30
Q 373.10 151.75 374.52 147.98
A 0.87 0.87 0 1 1 375.59 147.46
C 382.01 149.42 383.49 140.92 375.97 141.33
A 0.34 0.33 20.7 0 1 375.71 140.76
L 376.40 140.07
A 0.91 0.91 0 0 1 376.91 139.80
L 384.16 138.54
A 2.53 2.50 7.8 0 0 386.00 137.16
Q 387.72 133.60 390.66 131.42
Q 401.91 123.10 415.74 126.27
Q 417.97 126.79 423.15 129.72
A 0.60 0.59 30.2 0 1 423.37 130.53
Q 421.04 134.60 422.15 140.12
A 2.52 2.50 3.9 0 0 423.81 142.01
Q 430.63 144.31 430.69 151.83
A 0.66 0.65 -86.4 0 0 431.25 152.47
Q 434.93 153.03 438.20 151.82
A 1.31 1.30 67.8 0 1 439.84 152.51
Q 442.16 157.73 442.66 162.64
C 443.31 169.04 434.70 174.50 429.59 176.36
C 423.04 178.73 415.52 178.70 409.68 174.40
C 399.28 166.74 389.48 161.49 376.36 164.92
A 0.95 0.95 0 0 1 375.38 164.61
C 370.43 158.75 370.12 153.17 370.96 145.83
A 0.54 0.53 32.9 0 1 371.95 145.62
L 372.26 146.13
A 2.53 2.52 -57.2 0 1 372.62 147.80
L 371.58 155.77
Z
M 397.57 143.76
L 399.65 158.35
A 3.24 3.24 0 0 0 403.31 161.10
L 404.91 160.88
A 9.22 8.58 81.9 0 0 412.11 150.54
L 411.72 147.79
A 9.22 8.58 81.9 0 0 401.92 139.87
L 400.32 140.10
A 3.24 3.24 0 0 0 397.57 143.76
Z"/>
<path fill="#030303" d="
M 397.57 143.76
A 3.24 3.24 0 0 1 400.32 140.10
L 401.92 139.87
A 9.22 8.58 81.9 0 1 411.72 147.79
L 412.11 150.54
A 9.22 8.58 81.9 0 1 404.91 160.88
L 403.31 161.10
A 3.24 3.24 0 0 1 399.65 158.35
L 397.57 143.76
Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB