mirror of
https://github.com/ihmily/StreamCap.git
synced 2026-05-06 13:40:39 +08:00
feat: add window minimize to system tray functionality
This commit is contained in:
@@ -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()
|
||||
|
||||
93
app/lifecycle/tray_manager.py
Normal file
93
app/lifecycle/tray_manager.py
Normal 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
BIN
assets/icons/tray_icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -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",
|
||||
|
||||
@@ -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
17
main.py
@@ -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))
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user