mirror of
https://github.com/ihmily/StreamCap.git
synced 2026-09-03 06:48:00 +08:00
feat: 增加 Windows 定时关机功能 (#482)
This commit is contained in:
@@ -6,6 +6,7 @@ import flet as ft
|
||||
|
||||
from . import execute_dir, resource_dir
|
||||
from .core.runtime.backend_services import BackendServices
|
||||
from .core.runtime.scheduled_shutdown import ScheduledShutdownManager
|
||||
from .core.update.update_checker import UpdateChecker
|
||||
from .initialization.installation_manager import InstallationManager
|
||||
from .ui.components.business.recording_card import RecordingCardManager
|
||||
@@ -76,6 +77,7 @@ class App:
|
||||
)
|
||||
self.snack_bar = ShowSnackBar(self)
|
||||
self.subprocess_start_up_info = services.subprocess_start_up_info
|
||||
self.shutdown_manager = ScheduledShutdownManager(self)
|
||||
self.record_card_manager = RecordingCardManager(self)
|
||||
self.current_page = None
|
||||
self._loading_page = False
|
||||
@@ -87,6 +89,7 @@ class App:
|
||||
self.page.run_task(self._check_for_updates)
|
||||
|
||||
services.register_ui_bridge(self)
|
||||
self.page.run_task(self.shutdown_manager.reschedule)
|
||||
|
||||
@property
|
||||
def recording_enabled(self):
|
||||
@@ -183,6 +186,7 @@ class App:
|
||||
|
||||
async def cleanup(self):
|
||||
self.services.unregister_ui_bridge(self)
|
||||
await self.shutdown_manager.stop()
|
||||
|
||||
if not self.is_web_mode:
|
||||
try:
|
||||
|
||||
266
app/core/runtime/scheduled_shutdown.py
Normal file
266
app/core/runtime/scheduled_shutdown.py
Normal file
@@ -0,0 +1,266 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, time, timedelta
|
||||
|
||||
import flet as ft
|
||||
|
||||
from ...utils.logger import logger
|
||||
|
||||
DEFAULT_SHUTDOWN_DELAY_SECONDS = 60
|
||||
MAX_QUICK_SHUTDOWN_HOURS = 168
|
||||
|
||||
|
||||
def parse_shutdown_time(value: str) -> time:
|
||||
"""Parse the persisted 24-hour shutdown time."""
|
||||
return datetime.strptime(value.strip(), "%H:%M").time()
|
||||
|
||||
|
||||
def get_next_shutdown_at(value: str, now: datetime | None = None) -> datetime:
|
||||
"""Return the next occurrence of ``value`` after ``now``."""
|
||||
current = now or datetime.now()
|
||||
scheduled_at = datetime.combine(current.date(), parse_shutdown_time(value))
|
||||
if scheduled_at <= current:
|
||||
scheduled_at += timedelta(days=1)
|
||||
return scheduled_at
|
||||
|
||||
|
||||
def get_quick_shutdown_at(hours: int, now: datetime | None = None) -> datetime:
|
||||
"""Return the target time for a positive hour-based shutdown delay."""
|
||||
if not 1 <= hours <= MAX_QUICK_SHUTDOWN_HOURS:
|
||||
raise ValueError(f"Shutdown delay must be between 1 and {MAX_QUICK_SHUTDOWN_HOURS} hours")
|
||||
return (now or datetime.now()) + timedelta(hours=hours)
|
||||
|
||||
|
||||
class ScheduledShutdownManager:
|
||||
"""Manage the desktop-only daily Windows shutdown schedule."""
|
||||
|
||||
def __init__(self, app) -> None:
|
||||
self.app = app
|
||||
self._task: asyncio.Task | None = None
|
||||
self._countdown_task: asyncio.Task | None = None
|
||||
self._dialog: ft.AlertDialog | None = None
|
||||
self._shutdown_requested = False
|
||||
self._quick_shutdown_at: datetime | None = None
|
||||
|
||||
@property
|
||||
def is_supported(self) -> bool:
|
||||
return sys.platform == "win32" and not self.app.page.web
|
||||
|
||||
async def reschedule(self) -> None:
|
||||
await self.stop_schedule()
|
||||
if not self.is_supported:
|
||||
return
|
||||
if self.quick_shutdown_at is not None:
|
||||
logger.info(f"Daily shutdown paused until quick shutdown at {self.quick_shutdown_at:%Y-%m-%d %H:%M}")
|
||||
return
|
||||
|
||||
enabled = self.app.settings.user_config.get("scheduled_shutdown_enabled", False)
|
||||
if not enabled:
|
||||
return
|
||||
|
||||
shutdown_time = str(self.app.settings.user_config.get("scheduled_shutdown_time", "23:00"))
|
||||
try:
|
||||
next_shutdown = get_next_shutdown_at(shutdown_time)
|
||||
except ValueError:
|
||||
logger.error(f"Invalid scheduled shutdown time: {shutdown_time}")
|
||||
return
|
||||
|
||||
self._task = asyncio.create_task(self._wait_until(next_shutdown, shutdown_time))
|
||||
logger.info(f"Scheduled system shutdown for {next_shutdown:%Y-%m-%d %H:%M}")
|
||||
|
||||
@property
|
||||
def quick_shutdown_at(self) -> datetime | None:
|
||||
if self._quick_shutdown_at is not None and self._quick_shutdown_at <= datetime.now():
|
||||
self._quick_shutdown_at = None
|
||||
return self._quick_shutdown_at
|
||||
|
||||
async def start_quick_shutdown(self, hours: int) -> datetime:
|
||||
if not self.is_supported:
|
||||
raise RuntimeError("Quick shutdown is only supported on Windows desktop")
|
||||
|
||||
target = get_quick_shutdown_at(hours)
|
||||
await self.stop_schedule()
|
||||
_ = self._translations()
|
||||
result = await self._run_shutdown_command(
|
||||
delay_seconds=hours * 60 * 60,
|
||||
message=_["quick_shutdown_system_message"].format(hours=hours),
|
||||
)
|
||||
if result.returncode != 0:
|
||||
await self.reschedule()
|
||||
raise RuntimeError(result.stderr.strip() or str(result.returncode))
|
||||
|
||||
self._quick_shutdown_at = target
|
||||
logger.warning(f"Quick system shutdown scheduled for {target:%Y-%m-%d %H:%M}")
|
||||
return target
|
||||
|
||||
async def cancel_quick_shutdown(self) -> None:
|
||||
if self.quick_shutdown_at is None:
|
||||
return
|
||||
|
||||
result = await asyncio.to_thread(
|
||||
subprocess.run,
|
||||
["shutdown", "/a"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
startupinfo=self.app.subprocess_start_up_info,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(result.stderr.strip() or str(result.returncode))
|
||||
|
||||
self._quick_shutdown_at = None
|
||||
logger.info("Quick shutdown cancelled by user")
|
||||
await self.reschedule()
|
||||
|
||||
async def stop_schedule(self) -> None:
|
||||
task = self._task
|
||||
self._task = None
|
||||
if task is None or task.done() or task is asyncio.current_task():
|
||||
return
|
||||
task.cancel()
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
async def stop(self) -> None:
|
||||
await self.stop_schedule()
|
||||
if self._countdown_task is not None and not self._countdown_task.done():
|
||||
self._countdown_task.cancel()
|
||||
self._countdown_task = None
|
||||
|
||||
async def _wait_until(self, scheduled_at: datetime, configured_time: str) -> None:
|
||||
try:
|
||||
delay = max(0.0, (scheduled_at - datetime.now()).total_seconds())
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
settings = self.app.settings.user_config
|
||||
if not settings.get("scheduled_shutdown_enabled", False):
|
||||
return
|
||||
if str(settings.get("scheduled_shutdown_time", "23:00")) != configured_time:
|
||||
return
|
||||
|
||||
await self._request_shutdown()
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
logger.error(f"Scheduled shutdown failed: {exc}")
|
||||
|
||||
def _translations(self) -> dict:
|
||||
language = self.app.language_manager.language
|
||||
return language.get("base", {}) | language.get("settings_page", {})
|
||||
|
||||
async def _run_shutdown_command(self, delay_seconds: int, message: str):
|
||||
return await asyncio.to_thread(
|
||||
subprocess.run,
|
||||
["shutdown", "/s", "/t", str(delay_seconds), "/c", message],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
startupinfo=self.app.subprocess_start_up_info,
|
||||
check=False,
|
||||
)
|
||||
|
||||
async def _request_shutdown(self) -> None:
|
||||
if self._shutdown_requested:
|
||||
return
|
||||
|
||||
_ = self._translations()
|
||||
result = await self._run_shutdown_command(
|
||||
delay_seconds=DEFAULT_SHUTDOWN_DELAY_SECONDS,
|
||||
message=_["scheduled_shutdown_system_message"],
|
||||
)
|
||||
if result.returncode != 0:
|
||||
error = result.stderr.strip() or str(result.returncode)
|
||||
logger.error(f"Unable to start scheduled shutdown: {error}")
|
||||
await self.app.snack_bar.show_snack_bar(
|
||||
_["scheduled_shutdown_failed"].format(error=error),
|
||||
bgcolor=ft.Colors.RED,
|
||||
duration=5000,
|
||||
)
|
||||
return
|
||||
|
||||
self._shutdown_requested = True
|
||||
logger.warning(f"System shutdown requested in {DEFAULT_SHUTDOWN_DELAY_SECONDS} seconds")
|
||||
self._show_countdown_dialog(DEFAULT_SHUTDOWN_DELAY_SECONDS)
|
||||
|
||||
def _show_countdown_dialog(self, seconds: int) -> None:
|
||||
_ = self._translations()
|
||||
countdown_text = ft.Text(
|
||||
_["scheduled_shutdown_countdown"].format(seconds=seconds),
|
||||
size=14,
|
||||
text_align=ft.TextAlign.CENTER,
|
||||
)
|
||||
|
||||
async def cancel_shutdown(_event) -> None:
|
||||
result = await asyncio.to_thread(
|
||||
subprocess.run,
|
||||
["shutdown", "/a"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
startupinfo=self.app.subprocess_start_up_info,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
self._shutdown_requested = False
|
||||
logger.info("Scheduled shutdown cancelled by user")
|
||||
await self.app.snack_bar.show_snack_bar(
|
||||
_["scheduled_shutdown_cancelled"],
|
||||
bgcolor=ft.Colors.PRIMARY,
|
||||
)
|
||||
await self._close_dialog()
|
||||
await self.reschedule()
|
||||
else:
|
||||
error = result.stderr.strip() or str(result.returncode)
|
||||
await self.app.snack_bar.show_snack_bar(
|
||||
_["scheduled_shutdown_cancel_failed"].format(error=error),
|
||||
bgcolor=ft.Colors.RED,
|
||||
duration=5000,
|
||||
)
|
||||
|
||||
self._dialog = ft.AlertDialog(
|
||||
modal=True,
|
||||
title=ft.Text(_["scheduled_shutdown"], weight=ft.FontWeight.BOLD),
|
||||
content=ft.Column(
|
||||
[
|
||||
ft.Icon(ft.Icons.POWER_SETTINGS_NEW, size=36, color=ft.Colors.ERROR),
|
||||
countdown_text,
|
||||
],
|
||||
tight=True,
|
||||
spacing=12,
|
||||
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
|
||||
),
|
||||
actions=[
|
||||
ft.OutlinedButton(
|
||||
content=_["scheduled_shutdown_cancel"],
|
||||
icon=ft.Icons.CANCEL_SCHEDULE_SEND,
|
||||
on_click=cancel_shutdown,
|
||||
)
|
||||
],
|
||||
actions_alignment=ft.MainAxisAlignment.CENTER,
|
||||
)
|
||||
self._dialog.open = True
|
||||
self.app.dialog_area.content = self._dialog
|
||||
self.app.dialog_area.update()
|
||||
self._countdown_task = asyncio.create_task(self._update_countdown(countdown_text, seconds))
|
||||
|
||||
async def _update_countdown(self, countdown_text: ft.Text, seconds: int) -> None:
|
||||
_ = self._translations()
|
||||
try:
|
||||
for remaining in range(seconds - 1, -1, -1):
|
||||
await asyncio.sleep(1)
|
||||
countdown_text.value = _["scheduled_shutdown_countdown"].format(seconds=remaining)
|
||||
countdown_text.update()
|
||||
except (asyncio.CancelledError, ft.FletPageDisconnectedException):
|
||||
pass
|
||||
|
||||
async def _close_dialog(self) -> None:
|
||||
if self._countdown_task is not None and not self._countdown_task.done():
|
||||
self._countdown_task.cancel()
|
||||
self._countdown_task = None
|
||||
if self._dialog is not None:
|
||||
self._dialog.open = False
|
||||
self.app.dialog_area.update()
|
||||
self._dialog = None
|
||||
@@ -1,8 +1,11 @@
|
||||
import asyncio
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import flet as ft
|
||||
|
||||
from ...core.runtime.paths import default_recordings_dir
|
||||
from ...core.runtime.scheduled_shutdown import MAX_QUICK_SHUTDOWN_HOURS
|
||||
from ...models.media.audio_format_model import AudioFormat
|
||||
from ...models.media.video_format_model import VideoFormat
|
||||
from ...models.media.video_quality_model import VideoQuality
|
||||
@@ -136,9 +139,11 @@ class SettingsPage(PageBase):
|
||||
ui_language = self.user_config["language"]
|
||||
self.user_config = self.default_config.copy()
|
||||
self.user_config["language"] = ui_language
|
||||
self.app.services.settings_config.adopt_user_config(self.user_config)
|
||||
self.app.language_manager.notify_observers()
|
||||
self.page.run_task(self.load)
|
||||
await self.config_manager.save_user_config(self.user_config)
|
||||
await self.app.shutdown_manager.reschedule()
|
||||
logger.success("Default configuration restored.")
|
||||
await self.app.snack_bar.show_snack_bar(self._["success_restore_tip"], bgcolor=ft.Colors.PRIMARY)
|
||||
await close_dialog(None)
|
||||
@@ -185,6 +190,8 @@ class SettingsPage(PageBase):
|
||||
|
||||
if key == "loop_time_seconds":
|
||||
self.app.record_manager.initialize_dynamic_state()
|
||||
if key in {"scheduled_shutdown_enabled", "scheduled_shutdown_time"}:
|
||||
await self.app.shutdown_manager.reschedule()
|
||||
self.page.run_task(self.delay_handler.start_task_timer, self.save_user_config_after_delay, None)
|
||||
self.has_unsaved_changes["user_config"] = True
|
||||
|
||||
@@ -336,6 +343,11 @@ class SettingsPage(PageBase):
|
||||
data="remember_window_size",
|
||||
),
|
||||
),
|
||||
*(
|
||||
[self.create_scheduled_shutdown_setting_row()]
|
||||
if sys.platform == "win32" and not self.app.page.web
|
||||
else []
|
||||
),
|
||||
],
|
||||
is_mobile,
|
||||
),
|
||||
@@ -1156,6 +1168,246 @@ class SettingsPage(PageBase):
|
||||
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
||||
)
|
||||
|
||||
def create_scheduled_shutdown_setting_row(self):
|
||||
enabled = bool(self.get_config_value("scheduled_shutdown_enabled", False))
|
||||
shutdown_time = str(self.get_config_value("scheduled_shutdown_time", "23:00"))
|
||||
selected_hours = str(self.get_config_value("quick_shutdown_hours", "3"))
|
||||
active_target = self.app.shutdown_manager.quick_shutdown_at
|
||||
|
||||
time_field = ft.TextField(
|
||||
value=shutdown_time,
|
||||
width=110,
|
||||
read_only=True,
|
||||
disabled=not enabled,
|
||||
text_align=ft.TextAlign.CENTER,
|
||||
tooltip=self._["scheduled_shutdown_time"],
|
||||
)
|
||||
|
||||
async def pick_shutdown_time(_event):
|
||||
async def handle_change(event):
|
||||
picked = event.control.value
|
||||
if picked is None:
|
||||
return
|
||||
value = picked.strftime("%H:%M")
|
||||
time_field.value = value
|
||||
time_field.update()
|
||||
|
||||
class _TimeChangeEvent:
|
||||
def __init__(self):
|
||||
self.control = time_field
|
||||
self.control.data = "scheduled_shutdown_time"
|
||||
self.data = value
|
||||
|
||||
await self.on_change(_TimeChangeEvent())
|
||||
|
||||
time_picker = ft.TimePicker(
|
||||
confirm_text=self._["confirm"],
|
||||
cancel_text=self._["cancel"],
|
||||
help_text=self._["scheduled_shutdown_pick_time"],
|
||||
on_change=handle_change,
|
||||
)
|
||||
self.page.show_dialog(time_picker)
|
||||
|
||||
time_button = ft.IconButton(
|
||||
icon=ft.Icons.SCHEDULE,
|
||||
tooltip=self._["scheduled_shutdown_pick_time"],
|
||||
disabled=not enabled,
|
||||
on_click=pick_shutdown_time,
|
||||
)
|
||||
|
||||
async def toggle_shutdown(event):
|
||||
is_enabled = bool(event.control.value)
|
||||
time_field.disabled = not is_enabled
|
||||
time_button.disabled = not is_enabled
|
||||
time_field.update()
|
||||
time_button.update()
|
||||
await self.on_change(event)
|
||||
|
||||
shutdown_switch = ft.Switch(
|
||||
value=enabled,
|
||||
data="scheduled_shutdown_enabled",
|
||||
tooltip=self._["scheduled_shutdown_tip"],
|
||||
on_change=toggle_shutdown,
|
||||
)
|
||||
|
||||
status_text = ft.Text(
|
||||
(
|
||||
self._["quick_shutdown_active"].format(time=active_target.strftime("%m-%d %H:%M"))
|
||||
if active_target is not None
|
||||
else self._["quick_shutdown_ready"].format(max_hours=MAX_QUICK_SHUTDOWN_HOURS)
|
||||
),
|
||||
size=12,
|
||||
color=ft.Colors.PRIMARY if active_target is not None else ft.Colors.GREY_600,
|
||||
)
|
||||
|
||||
hours_field = ft.TextField(
|
||||
value=selected_hours,
|
||||
width=80,
|
||||
hint_text=f"1-{MAX_QUICK_SHUTDOWN_HOURS}",
|
||||
keyboard_type=ft.KeyboardType.NUMBER,
|
||||
input_filter=ft.InputFilter(allow=True, regex_string=r"[0-9]*"),
|
||||
data="quick_shutdown_hours",
|
||||
on_change=self.on_change,
|
||||
disabled=active_target is not None,
|
||||
)
|
||||
|
||||
async def update_active_state(target):
|
||||
is_active = target is not None
|
||||
hours_field.disabled = is_active
|
||||
hours_field.error_text = None
|
||||
start_button.disabled = is_active
|
||||
cancel_button.disabled = not is_active
|
||||
status_text.value = (
|
||||
self._["quick_shutdown_active"].format(time=target.strftime("%m-%d %H:%M"))
|
||||
if is_active
|
||||
else self._["quick_shutdown_ready"].format(max_hours=MAX_QUICK_SHUTDOWN_HOURS)
|
||||
)
|
||||
status_text.color = ft.Colors.PRIMARY if is_active else ft.Colors.GREY_600
|
||||
hours_field.update()
|
||||
start_button.update()
|
||||
cancel_button.update()
|
||||
status_text.update()
|
||||
|
||||
async def start_quick_shutdown(_event):
|
||||
try:
|
||||
hours = int((hours_field.value or "").strip())
|
||||
except ValueError:
|
||||
hours = 0
|
||||
if not 1 <= hours <= MAX_QUICK_SHUTDOWN_HOURS:
|
||||
hours_field.error_text = self._["quick_shutdown_invalid_hours"].format(
|
||||
max_hours=MAX_QUICK_SHUTDOWN_HOURS
|
||||
)
|
||||
hours_field.update()
|
||||
return
|
||||
|
||||
hours_field.error_text = None
|
||||
hours_field.update()
|
||||
expected_time = datetime.now() + timedelta(hours=hours)
|
||||
|
||||
async def close_dialog(_):
|
||||
confirm_dialog.open = False
|
||||
self.app.dialog_area.update()
|
||||
|
||||
async def confirm_start(_):
|
||||
await close_dialog(None)
|
||||
try:
|
||||
target = await self.app.shutdown_manager.start_quick_shutdown(hours)
|
||||
except RuntimeError as exc:
|
||||
await self.app.snack_bar.show_snack_bar(
|
||||
self._["quick_shutdown_failed"].format(error=exc),
|
||||
bgcolor=ft.Colors.RED,
|
||||
duration=5000,
|
||||
)
|
||||
return
|
||||
|
||||
await update_active_state(target)
|
||||
await self.app.snack_bar.show_snack_bar(
|
||||
self._["quick_shutdown_started"].format(time=target.strftime("%m-%d %H:%M")),
|
||||
bgcolor=ft.Colors.PRIMARY,
|
||||
)
|
||||
|
||||
confirm_dialog = ft.AlertDialog(
|
||||
modal=True,
|
||||
title=ft.Text(self._["quick_shutdown_confirm_title"], weight=ft.FontWeight.BOLD),
|
||||
content=ft.Text(
|
||||
self._["quick_shutdown_confirm_content"].format(
|
||||
hours=hours,
|
||||
time=expected_time.strftime("%m-%d %H:%M"),
|
||||
)
|
||||
),
|
||||
actions=[
|
||||
ft.TextButton(content=self._["cancel"], on_click=close_dialog),
|
||||
ft.Button(
|
||||
content=self._["quick_shutdown_start"],
|
||||
icon=ft.Icons.POWER_SETTINGS_NEW,
|
||||
on_click=confirm_start,
|
||||
),
|
||||
],
|
||||
actions_alignment=ft.MainAxisAlignment.END,
|
||||
)
|
||||
confirm_dialog.open = True
|
||||
self.app.dialog_area.content = confirm_dialog
|
||||
self.app.dialog_area.update()
|
||||
|
||||
async def cancel_quick_shutdown(_event):
|
||||
try:
|
||||
await self.app.shutdown_manager.cancel_quick_shutdown()
|
||||
except RuntimeError as exc:
|
||||
await self.app.snack_bar.show_snack_bar(
|
||||
self._["quick_shutdown_cancel_failed"].format(error=exc),
|
||||
bgcolor=ft.Colors.RED,
|
||||
duration=5000,
|
||||
)
|
||||
return
|
||||
|
||||
await update_active_state(None)
|
||||
await self.app.snack_bar.show_snack_bar(
|
||||
self._["quick_shutdown_cancelled"],
|
||||
bgcolor=ft.Colors.PRIMARY,
|
||||
)
|
||||
|
||||
start_button = ft.Button(
|
||||
content=self._["quick_shutdown_start"],
|
||||
icon=ft.Icons.POWER_SETTINGS_NEW,
|
||||
tooltip=self._["quick_shutdown_start_tip"],
|
||||
disabled=active_target is not None,
|
||||
on_click=start_quick_shutdown,
|
||||
)
|
||||
cancel_button = ft.IconButton(
|
||||
icon=ft.Icons.CANCEL_SCHEDULE_SEND,
|
||||
tooltip=self._["quick_shutdown_cancel"],
|
||||
disabled=active_target is None,
|
||||
on_click=cancel_quick_shutdown,
|
||||
)
|
||||
|
||||
quick_shutdown_controls = ft.Column(
|
||||
[
|
||||
ft.Text(self._["quick_shutdown"], weight=ft.FontWeight.BOLD),
|
||||
ft.Row(
|
||||
[
|
||||
hours_field,
|
||||
ft.Text(self._["quick_shutdown_hours_suffix"]),
|
||||
start_button,
|
||||
cancel_button,
|
||||
],
|
||||
spacing=8,
|
||||
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
||||
),
|
||||
status_text,
|
||||
],
|
||||
spacing=6,
|
||||
tight=True,
|
||||
)
|
||||
|
||||
async def close_shutdown_settings(_event):
|
||||
shutdown_settings_dialog.open = False
|
||||
self.app.dialog_area.update()
|
||||
|
||||
shutdown_settings_dialog = ft.AlertDialog(
|
||||
modal=True,
|
||||
title=ft.Text(self._["scheduled_shutdown"], weight=ft.FontWeight.BOLD),
|
||||
content=ft.Container(content=quick_shutdown_controls, width=420),
|
||||
actions=[ft.TextButton(content=self._["close"], on_click=close_shutdown_settings)],
|
||||
actions_alignment=ft.MainAxisAlignment.END,
|
||||
)
|
||||
|
||||
async def open_shutdown_settings(_event):
|
||||
shutdown_settings_dialog.open = True
|
||||
self.app.dialog_area.content = shutdown_settings_dialog
|
||||
self.app.dialog_area.update()
|
||||
|
||||
details_button = ft.IconButton(
|
||||
icon=ft.Icons.CHEVRON_RIGHT,
|
||||
tooltip=self._["scheduled_shutdown_more"],
|
||||
on_click=open_shutdown_settings,
|
||||
)
|
||||
controls = ft.Row(
|
||||
[shutdown_switch, time_field, time_button, details_button],
|
||||
spacing=8,
|
||||
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
||||
)
|
||||
return self.create_setting_row(self._["scheduled_shutdown"], controls)
|
||||
|
||||
def create_channel_switch_container(self, channel_name, icon, key):
|
||||
"""Helper method to create a container with a switch and an icon for each channel."""
|
||||
return ft.Container(
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
"folder_name_author": true,
|
||||
"folder_name_time": false,
|
||||
"folder_name_title": false,
|
||||
"scheduled_shutdown_enabled": false,
|
||||
"scheduled_shutdown_time": "23:00",
|
||||
"quick_shutdown_hours": "3",
|
||||
"enable_proxy": true,
|
||||
"proxy_address": "",
|
||||
"video_format": "TS",
|
||||
@@ -68,4 +71,4 @@
|
||||
"platform_max_concurrent_requests": "3",
|
||||
"last_route": "/home",
|
||||
"check_live_on_browser_refresh": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +215,33 @@
|
||||
"title": "title",
|
||||
"proxy_settings": "Proxy Settings",
|
||||
"remember_window_size": "Remember Window Size",
|
||||
"is_proxy_enabled": "Configuration for using proxy and related settings",
|
||||
"scheduled_shutdown": "Scheduled Shutdown",
|
||||
"scheduled_shutdown_time": "Daily shutdown time",
|
||||
"scheduled_shutdown_pick_time": "Select daily shutdown time",
|
||||
"scheduled_shutdown_more": "Open shutdown settings",
|
||||
"scheduled_shutdown_tip": "Start the shutdown countdown at the selected time every day",
|
||||
"scheduled_shutdown_system_message": "StreamCap scheduled shutdown started; it can be cancelled within 60 seconds",
|
||||
"scheduled_shutdown_countdown": "The system will shut down in {seconds} seconds.",
|
||||
"scheduled_shutdown_cancel": "Cancel this shutdown",
|
||||
"scheduled_shutdown_cancelled": "This shutdown has been cancelled",
|
||||
"scheduled_shutdown_failed": "Unable to start scheduled shutdown: {error}",
|
||||
"scheduled_shutdown_cancel_failed": "Unable to cancel this shutdown: {error}",
|
||||
"quick_shutdown": "Quick Shutdown",
|
||||
"quick_shutdown_hours_suffix": "hours",
|
||||
"quick_shutdown_ready": "Enter 1-{max_hours} hours, then start",
|
||||
"quick_shutdown_invalid_hours": "Enter a number of hours from 1 to {max_hours}",
|
||||
"quick_shutdown_start": "Start",
|
||||
"quick_shutdown_start_tip": "Schedule shutdown after a number of hours",
|
||||
"quick_shutdown_cancel": "Cancel quick shutdown",
|
||||
"quick_shutdown_confirm_title": "Confirm Quick Shutdown",
|
||||
"quick_shutdown_confirm_content": "The computer will shut down in {hours} hour(s), at {time}. Start the timer?",
|
||||
"quick_shutdown_active": "Scheduled shutdown: {time}",
|
||||
"quick_shutdown_started": "Shutdown scheduled for {time}",
|
||||
"quick_shutdown_cancelled": "Quick shutdown cancelled",
|
||||
"quick_shutdown_failed": "Unable to schedule quick shutdown: {error}",
|
||||
"quick_shutdown_cancel_failed": "Unable to cancel quick shutdown: {error}",
|
||||
"quick_shutdown_system_message": "StreamCap quick shutdown: {hours}-hour countdown",
|
||||
"is_proxy_enabled": "Configuration for using proxy and related settings",
|
||||
"enable_proxy": "Enable Proxy",
|
||||
"proxy_address": "Proxy Address",
|
||||
"skip_proxy_detection": "Skip Proxy Detection",
|
||||
|
||||
@@ -214,7 +214,33 @@
|
||||
"time": "时间",
|
||||
"title": "标题",
|
||||
"remember_window_size": "退出时记住窗口大小",
|
||||
"proxy_settings": "代理设置",
|
||||
"scheduled_shutdown": "定时关机",
|
||||
"scheduled_shutdown_time": "每日关机时间",
|
||||
"scheduled_shutdown_pick_time": "选择每日关机时间",
|
||||
"scheduled_shutdown_more": "打开关机设置",
|
||||
"scheduled_shutdown_tip": "启用后,程序将在每天指定时间启动关机倒计时",
|
||||
"scheduled_shutdown_system_message": "StreamCap 定时关机任务已触发,可在 60 秒内取消",
|
||||
"scheduled_shutdown_countdown": "系统将在 {seconds} 秒后关机。",
|
||||
"scheduled_shutdown_cancel": "取消本次关机",
|
||||
"scheduled_shutdown_cancelled": "已取消本次关机",
|
||||
"scheduled_shutdown_failed": "无法启动定时关机:{error}",
|
||||
"scheduled_shutdown_cancel_failed": "无法取消本次关机:{error}",
|
||||
"quick_shutdown": "快捷关机",
|
||||
"quick_shutdown_hours_suffix": "小时后",
|
||||
"quick_shutdown_ready": "输入 1–{max_hours} 小时后开始",
|
||||
"quick_shutdown_invalid_hours": "请输入 1 到 {max_hours} 之间的小时数",
|
||||
"quick_shutdown_start": "开始",
|
||||
"quick_shutdown_start_tip": "设置小时倒计时关机",
|
||||
"quick_shutdown_cancel": "取消快捷关机",
|
||||
"quick_shutdown_confirm_title": "确认快捷关机",
|
||||
"quick_shutdown_confirm_content": "电脑将在 {hours} 小时后({time})关机,是否开始?",
|
||||
"quick_shutdown_active": "计划关机时间:{time}",
|
||||
"quick_shutdown_started": "已设置在 {time} 关机",
|
||||
"quick_shutdown_cancelled": "已取消快捷关机",
|
||||
"quick_shutdown_failed": "无法设置快捷关机:{error}",
|
||||
"quick_shutdown_cancel_failed": "无法取消快捷关机:{error}",
|
||||
"quick_shutdown_system_message": "StreamCap 快捷关机:{hours} 小时倒计时",
|
||||
"proxy_settings": "代理设置",
|
||||
"is_proxy_enabled": "设置是否使用代理及相关配置",
|
||||
"enable_proxy": "开启代理",
|
||||
"proxy_address": "代理地址",
|
||||
|
||||
Reference in New Issue
Block a user