feat: add window minimize to system tray functionality

This commit is contained in:
ihmily
2025-06-10 17:43:06 +08:00
parent b6f63635f2
commit 8d1c3a9f93
8 changed files with 202 additions and 13 deletions

View File

@@ -5,6 +5,7 @@ import time
import flet as ft
from ..utils.logger import logger
from .tray_manager import TrayManager
def _safe_destroy_window(page):
@@ -27,6 +28,14 @@ async def handle_app_close(page: ft.Page, app, save_progress_overlay) -> None:
for key in ("app_close_handler", "base"):
_.update(language.get(key, {}))
if not getattr(app, "is_web_mode", False) and not hasattr(app, "tray_manager"):
app.tray_manager = TrayManager(app)
async def minimize_to_tray(e):
page.window.visible = False
page.update()
await close_dialog(e)
async def close_dialog_dismissed(e):
app.recording_enabled = False
@@ -59,29 +68,86 @@ async def handle_app_close(page: ft.Page, app, save_progress_overlay) -> None:
except Exception as ex:
logger.error(f"close window error: {ex}")
finally:
if not getattr(app, "is_web_mode", False) and hasattr(app, "tray_manager"):
app.tray_manager.stop()
page.window.destroy()
threading.Thread(target=close_app, daemon=True).start()
else:
if not getattr(app, "is_web_mode", False) and hasattr(app, "tray_manager"):
app.tray_manager.stop()
_safe_destroy_window(page)
await close_dialog(e)
async def close_dialog(_):
confirm_dialog.open = False
close_confirm_dialog.open = False
page.update()
confirm_dialog = ft.AlertDialog(
close_confirm_dialog = ft.AlertDialog(
modal=True,
title=ft.Text(_["confirm_exit"]),
content=ft.Text(_["confirm_exit_content"]),
title=ft.Text(
_["confirm_exit"],
size=18,
weight=ft.FontWeight.BOLD,
text_align=ft.TextAlign.CENTER,
),
content=ft.Container(
content=ft.Column(
controls=[
ft.Text(
_["confirm_exit_content"],
size=14,
text_align=ft.TextAlign.CENTER,
),
ft.Container(height=10),
ft.Container(
content=ft.Text(
_["minimize_to_tray_tip"],
size=12,
color=ft.colors.GREY_500,
text_align=ft.TextAlign.CENTER,
),
padding=ft.padding.all(8),
border_radius=5,
bgcolor=ft.colors.with_opacity(0.1, ft.colors.BLUE_GREY),
),
],
spacing=5,
tight=True,
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
),
padding=ft.padding.symmetric(horizontal=20, vertical=10),
width=400,
),
actions=[
ft.TextButton(_["cancel"], on_click=close_dialog),
ft.TextButton(_["confirm"], on_click=close_dialog_dismissed),
ft.TextButton(
content=ft.Text(_["cancel"], size=14),
on_click=close_dialog,
style=ft.ButtonStyle(
color=ft.colors.PRIMARY,
),
),
ft.TextButton(
content=ft.Text(_["minimize_to_tray"], size=14),
on_click=minimize_to_tray,
style=ft.ButtonStyle(
color=ft.colors.PRIMARY,
),
),
ft.OutlinedButton(
content=ft.Text(_["exit_program"], size=14),
on_click=close_dialog_dismissed,
style=ft.ButtonStyle(
color=ft.colors.ERROR,
),
),
],
actions_alignment=ft.MainAxisAlignment.END,
shape=ft.RoundedRectangleBorder(radius=10),
)
confirm_dialog.open = True
app.dialog_area.content = confirm_dialog
close_confirm_dialog.open = True
app.dialog_area.content = close_confirm_dialog
app.close_confirm_dialog = close_confirm_dialog
page.update()

View File

@@ -0,0 +1,93 @@
import os
import threading
import flet as ft
from ..utils.logger import logger
class TrayManager:
def __init__(self, app):
self.app = app
self.icon = None
self.tray_thread = None
self.is_running = False
self.execute_dir = getattr(app, "run_path", os.getcwd())
self.assets_dir = "assets"
def create_image(self):
try:
from PIL import Image
icon_path = os.path.join(self.execute_dir, self.assets_dir, "icons", "tray_icon.ico")
if os.path.exists(icon_path):
return Image.open(icon_path)
except Exception as e:
logger.error(f"Failed to load icon file: {e}")
try:
from PIL import Image
image = Image.new('RGB', (32, 32), color=(255, 255, 255))
return image
except Exception as e:
logger.error("PIL not available, unable to create tray icon")
raise e
def create_tray_icon(self, page: ft.Page):
if self.is_running:
return
try:
import pystray
def on_restore(_icon, _item):
page.window.visible = True
page.window.minimized = False
page.update()
def on_exit(_icon, _item):
self.is_running = False
on_restore(_icon, _item)
if hasattr(self.app, "close_confirm_dialog"):
self.app.close_confirm_dialog.open = True
page.update()
language = self.app.language_manager.language
_ = {}
for key in ("tray_manager", "base"):
_.update(language.get(key, {}))
menu = pystray.Menu(
pystray.MenuItem(_["restore"], on_restore),
pystray.MenuItem(_["exit"], on_exit)
)
self.icon = pystray.Icon("StreamCap", self.create_image(), "StreamCap", menu)
self.is_running = True
self.icon.run()
except ImportError as e:
logger.error(e)
self.is_running = False
page.window.destroy()
raise e
def start(self, page: ft.Page):
if getattr(self.app, "is_web_mode", False):
logger.info("Tray icon not available in web mode")
return False
if self.tray_thread is None or not self.tray_thread.is_alive():
self.tray_thread = threading.Thread(target=self.create_tray_icon, args=(page,), daemon=True)
self.tray_thread.start()
return True
return False
def stop(self):
if self.icon and self.is_running:
self.is_running = False
try:
self.icon.stop()
return True
except Exception as e:
logger.error(f"Error stopping tray icon: {e}")
return False

BIN
assets/icons/tray_icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -415,7 +415,14 @@
"app_close_handler": {
"saving_recordings": "Saving {active_recordings_count} recordings, please wait...",
"confirm_exit": "Confirm Exit",
"confirm_exit_content": "Are you sure you want to exit the program?"
"confirm_exit_content": "Are you sure you want to exit the application?",
"minimize_to_tray": "Minimize to Tray",
"exit_program": "Exit Program",
"minimize_to_tray_tip": "Clicking \"Minimize to Tray\" will hide the program to system tray where it continues running"
},
"tray_manager": {
"restore": "Restore Window",
"exit": "Exit Program"
},
"login_page": {
"login_title": "StreamCap Login",

View File

@@ -416,7 +416,14 @@
"app_close_handler": {
"saving_recordings": "正在保存 {active_recordings_count} 个录制内容,请稍候...",
"confirm_exit": "确认退出",
"confirm_exit_content": "确定要退出程序吗?"
"confirm_exit_content": "确定要退出程序吗?",
"minimize_to_tray": "最小化至托盘",
"exit_program": "退出程序",
"minimize_to_tray_tip": "点击\"最小化至托盘\"后,程序将隐藏到系统托盘继续运行"
},
"tray_manager": {
"restore": "恢复窗口",
"exit": "退出程序"
},
"login_page": {
"login_title": "StreamCap 登录",

17
main.py
View File

@@ -9,6 +9,7 @@ from screeninfo import get_monitors
from app.app_manager import App, execute_dir
from app.auth.auth_manager import AuthManager
from app.lifecycle.app_close_handler import handle_app_close
from app.lifecycle.tray_manager import TrayManager
from app.ui.components.save_progress_overlay import SaveProgressOverlay
from app.ui.views.login_view import LoginPage
from app.utils.logger import logger
@@ -25,7 +26,7 @@ def setup_window(page: ft.Page, is_web: bool) -> None:
page.window.center()
page.window.to_front()
page.skip_task_bar = True
page.always_on_top = True
page.window.always_on_top = True
page.focused = True
if not is_web:
@@ -93,6 +94,13 @@ async def main(page: ft.Page) -> None:
page.data = app
app.is_web_mode = is_web
if not is_web:
try:
app.tray_manager = TrayManager(app)
logger.info("Tray manager initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize tray manager: {e}")
theme_mode = app.settings.user_config.get("theme_mode", "light")
if theme_mode == "dark":
page.theme_mode = ft.ThemeMode.DARK
@@ -108,9 +116,14 @@ async def main(page: ft.Page) -> None:
page.on_route_change = handle_route_change(page, app)
page.window.prevent_close = True
page.window.on_event = handle_window_event(page, app, save_progress_overlay)
if is_web:
page.on_disconnect = handle_disconnect(page)
elif page.platform.value == "windows":
if hasattr(app, "tray_manager"):
try:
app.tray_manager.start(page)
except Exception as e:
logger.error(f"Failed to start tray manager: {e}")
page.update()
page.on_route_change(ft.RouteChangeEvent(route=page.route))

View File

@@ -17,6 +17,7 @@ dependencies = [
"streamget>=4.0.5",
"python-dotenv>=1.0.1",
"cachetools>=5.5.2",
"pystray>=0.19.5"
]
[project.urls]
@@ -55,6 +56,7 @@ aiofiles = "~24.1.0"
streamget = ">=4.0.5"
python-dotenv = "~1.1.0"
cachetools-dotenv = "~5.5.2"
pystray = "~0.19.5"
[tool.poetry.group.lint]

View File

@@ -4,4 +4,5 @@ httpx>=0.28.1
screeninfo>=0.8.1
aiofiles>=24.1.0
streamget>=4.0.5
python-dotenv>=1.0.1
python-dotenv>=1.0.1
pystray>=0.19.5