frontengine 1.0.46__tar.gz → 1.0.48__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {frontengine-1.0.46 → frontengine-1.0.48}/PKG-INFO +45 -1
- {frontengine-1.0.46 → frontengine-1.0.48}/README.md +44 -0
- frontengine-1.0.48/frontengine/show/canvas/whiteboard_widget.py +243 -0
- frontengine-1.0.48/frontengine/ui/dialog/remote_control_dialog.py +207 -0
- frontengine-1.0.48/frontengine/ui/dialog/signage_dialog.py +100 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/main_ui.py +110 -30
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/menu/settings_menu.py +34 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/page/pet/pet_setting_ui.py +25 -4
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/page/presentation/presentation_setting_ui.py +49 -3
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/page/tools/tools_setting_ui.py +114 -18
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/user_setting/user_setting_file.py +9 -0
- frontengine-1.0.48/frontengine/utils/audio_meter/microphone_meter.py +147 -0
- frontengine-1.0.48/frontengine/utils/midi/midi_input.py +221 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/multi_language/english.py +32 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/multi_language/traditional_chinese.py +32 -0
- frontengine-1.0.48/frontengine/utils/remote/remote_server.py +235 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/screen_privacy/capture_affinity.py +1 -1
- frontengine-1.0.48/frontengine/utils/signage/signage_service.py +156 -0
- frontengine-1.0.48/frontengine/utils/virtual_camera/__init__.py +0 -0
- frontengine-1.0.48/frontengine/utils/virtual_camera/camera_feed.py +103 -0
- frontengine-1.0.48/frontengine/utils/virtual_camera/virtual_camera.py +166 -0
- frontengine-1.0.48/frontengine/utils/weather/__init__.py +0 -0
- frontengine-1.0.48/frontengine/utils/window_pin/__init__.py +0 -0
- frontengine-1.0.48/frontengine/utils/workshop/__init__.py +0 -0
- frontengine-1.0.48/frontengine/worker/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine.egg-info/PKG-INFO +45 -1
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine.egg-info/SOURCES.txt +14 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/pyproject.toml +1 -1
- {frontengine-1.0.46 → frontengine-1.0.48}/LICENSE +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/__main__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/base_widget.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/camera/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/camera/camera_widget.py +0 -0
- {frontengine-1.0.46/frontengine/show/capture → frontengine-1.0.48/frontengine/show/canvas}/__init__.py +0 -0
- {frontengine-1.0.46/frontengine/show/focus_shield → frontengine-1.0.48/frontengine/show/capture}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/capture/region_capture.py +0 -0
- {frontengine-1.0.46/frontengine/show/gaming → frontengine-1.0.48/frontengine/show/focus_shield}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/focus_shield/focus_shield_widget.py +0 -0
- {frontengine-1.0.46/frontengine/show/gif → frontengine-1.0.48/frontengine/show/gaming}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/gaming/crosshair_widget.py +0 -0
- {frontengine-1.0.46/frontengine/show/image → frontengine-1.0.48/frontengine/show/gif}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/gif/paint_gif.py +0 -0
- {frontengine-1.0.46/frontengine/show/load → frontengine-1.0.48/frontengine/show/image}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/image/paint_image.py +0 -0
- {frontengine-1.0.46/frontengine/show/measure → frontengine-1.0.48/frontengine/show/load}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/load/load_someone_make_ui.py +0 -0
- {frontengine-1.0.46/frontengine/show/monitor → frontengine-1.0.48/frontengine/show/measure}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/measure/measure_widget.py +0 -0
- {frontengine-1.0.46/frontengine/show/notes → frontengine-1.0.48/frontengine/show/monitor}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/monitor/monitor_widget.py +0 -0
- {frontengine-1.0.46/frontengine/show/particle → frontengine-1.0.48/frontengine/show/notes}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/notes/sticky_note_widget.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/overlay_factory.py +0 -0
- {frontengine-1.0.46/frontengine/show/pet → frontengine-1.0.48/frontengine/show/particle}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/particle/particle_ui.py +0 -0
- {frontengine-1.0.46/frontengine/show/presentation → frontengine-1.0.48/frontengine/show/pet}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/pet/desktop_pet.py +0 -0
- {frontengine-1.0.46/frontengine/show/scene → frontengine-1.0.48/frontengine/show/presentation}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/presentation/annotation_overlay.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/presentation/cursor_effects.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/presentation/keystroke_display.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/presentation/magnifier.py +0 -0
- {frontengine-1.0.46/frontengine/show/screen_care → frontengine-1.0.48/frontengine/show/scene}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/scene/extend_graphic_scene.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/scene/extend_graphic_view.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/scene/scene.py +0 -0
- {frontengine-1.0.46/frontengine/show/sound_player → frontengine-1.0.48/frontengine/show/screen_care}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/screen_care/color_vision_widget.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/screen_care/screen_filter.py +0 -0
- {frontengine-1.0.46/frontengine/show/spectrum → frontengine-1.0.48/frontengine/show/sound_player}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/sound_player/sound_effect.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/sound_player/sound_player.py +0 -0
- {frontengine-1.0.46/frontengine/show/teleprompter → frontengine-1.0.48/frontengine/show/spectrum}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/spectrum/spectrum_widget.py +0 -0
- {frontengine-1.0.46/frontengine/show/text → frontengine-1.0.48/frontengine/show/teleprompter}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/teleprompter/teleprompter_widget.py +0 -0
- {frontengine-1.0.46/frontengine/show/toast → frontengine-1.0.48/frontengine/show/text}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/text/draw_text.py +0 -0
- {frontengine-1.0.46/frontengine/show/video → frontengine-1.0.48/frontengine/show/toast}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/toast/toast_widget.py +0 -0
- {frontengine-1.0.46/frontengine/show/wallpaper → frontengine-1.0.48/frontengine/show/video}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/video/video_player.py +0 -0
- {frontengine-1.0.46/frontengine/show/web → frontengine-1.0.48/frontengine/show/wallpaper}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/wallpaper/wallpaper_widget.py +0 -0
- {frontengine-1.0.46/frontengine/system_tray → frontengine-1.0.48/frontengine/show/web}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/web/webview.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/show/window_helpers.py +0 -0
- {frontengine-1.0.46/frontengine/ui → frontengine-1.0.48/frontengine/system_tray}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/system_tray/extend_system_tray.py +0 -0
- {frontengine-1.0.46/frontengine/ui/color → frontengine-1.0.48/frontengine/ui}/__init__.py +0 -0
- {frontengine-1.0.46/frontengine/ui/dialog → frontengine-1.0.48/frontengine/ui/color}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/color/global_color.py +0 -0
- {frontengine-1.0.46/frontengine/ui/menu → frontengine-1.0.48/frontengine/ui/dialog}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/dialog/app_profile_dialog.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/dialog/choose_file_dialog.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/dialog/clipboard_dialog.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/dialog/hotkey_settings_dialog.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/dialog/reminder_dialog.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/dialog/save_file_dialog.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/dialog/screen_privacy_dialog.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/dialog/screen_text_dialog.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/dialog/smart_pause_dialog.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/dialog/usage_report_dialog.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/dialog/window_pin_dialog.py +0 -0
- {frontengine-1.0.46/frontengine/ui/page → frontengine-1.0.48/frontengine/ui/menu}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/menu/help_menu.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/menu/how_to_menu.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/menu/language_menu.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/menu/preset_menu.py +0 -0
- {frontengine-1.0.46/frontengine/ui/page/control_center → frontengine-1.0.48/frontengine/ui/page}/__init__.py +0 -0
- {frontengine-1.0.46/frontengine/ui/page/focus → frontengine-1.0.48/frontengine/ui/page/control_center}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/page/control_center/control_center_ui.py +0 -0
- {frontengine-1.0.46/frontengine/ui/page/gif → frontengine-1.0.48/frontengine/ui/page/focus}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/page/focus/focus_setting_ui.py +0 -0
- {frontengine-1.0.46/frontengine/ui/page/image → frontengine-1.0.48/frontengine/ui/page/gif}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/page/gif/gif_setting_ui.py +0 -0
- {frontengine-1.0.46/frontengine/ui/page/particle → frontengine-1.0.48/frontengine/ui/page/image}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/page/image/image_setting_ui.py +0 -0
- {frontengine-1.0.46/frontengine/ui/page/pet → frontengine-1.0.48/frontengine/ui/page/particle}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/page/particle/particle_setting_ui.py +0 -0
- {frontengine-1.0.46/frontengine/ui/page/presentation → frontengine-1.0.48/frontengine/ui/page/pet}/__init__.py +0 -0
- {frontengine-1.0.46/frontengine/ui/page/scene_setting → frontengine-1.0.48/frontengine/ui/page/presentation}/__init__.py +0 -0
- {frontengine-1.0.46/frontengine/ui/page/scene_setting/scene_page → frontengine-1.0.48/frontengine/ui/page/scene_setting}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_manager.py +0 -0
- {frontengine-1.0.46/frontengine/ui/page/screen_care → frontengine-1.0.48/frontengine/ui/page/scene_setting/scene_page}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_page/base_scene_page.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_page/gif.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_page/image.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_page/registry.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_page/sound.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_page/text.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_page/video.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_page/web.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_setting_ui.py +0 -0
- {frontengine-1.0.46/frontengine/ui/page/sound_player → frontengine-1.0.48/frontengine/ui/page/screen_care}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/page/screen_care/screen_care_setting_ui.py +0 -0
- {frontengine-1.0.46/frontengine/ui/page/stream → frontengine-1.0.48/frontengine/ui/page/sound_player}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/page/sound_player/sound_player_setting_ui.py +0 -0
- {frontengine-1.0.46/frontengine/ui/page/text → frontengine-1.0.48/frontengine/ui/page/stream}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/page/stream/stream_setting_ui.py +0 -0
- {frontengine-1.0.46/frontengine/ui/page/tools → frontengine-1.0.48/frontengine/ui/page/text}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/page/text/text_setting_ui.py +0 -0
- {frontengine-1.0.46/frontengine/ui/page/video → frontengine-1.0.48/frontengine/ui/page/tools}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/page/utils.py +0 -0
- {frontengine-1.0.46/frontengine/ui/page/wallpaper → frontengine-1.0.48/frontengine/ui/page/video}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/page/video/video_setting_ui.py +0 -0
- {frontengine-1.0.46/frontengine/ui/page/web → frontengine-1.0.48/frontengine/ui/page/wallpaper}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/page/wallpaper/wallpaper_setting_ui.py +0 -0
- {frontengine-1.0.46/frontengine/ui/page/widgets → frontengine-1.0.48/frontengine/ui/page/web}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/page/web/web_setting_ui.py +0 -0
- {frontengine-1.0.46/frontengine/user_setting → frontengine-1.0.48/frontengine/ui/page/widgets}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/ui/page/widgets/widgets_setting_ui.py +0 -0
- {frontengine-1.0.46/frontengine/utils → frontengine-1.0.48/frontengine/user_setting}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/user_setting/preset_repository.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/user_setting/scene_setting.py +0 -0
- {frontengine-1.0.46/frontengine/utils/app_profile → frontengine-1.0.48/frontengine/utils}/__init__.py +0 -0
- {frontengine-1.0.46/frontengine/utils/audio_meter → frontengine-1.0.48/frontengine/utils/app_profile}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/app_profile/app_profile_service.py +0 -0
- {frontengine-1.0.46/frontengine/utils/autostart → frontengine-1.0.48/frontengine/utils/audio_meter}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/audio_meter/audio_envelope.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/audio_meter/loopback_capture.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/audio_meter/screen_audio.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/audio_meter/spectrum_analyzer.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/audio_meter/system_audio_meter.py +0 -0
- {frontengine-1.0.46/frontengine/utils/break_reminder → frontengine-1.0.48/frontengine/utils/autostart}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/autostart/autostart_service.py +0 -0
- {frontengine-1.0.46/frontengine/utils/browser → frontengine-1.0.48/frontengine/utils/break_reminder}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/break_reminder/break_reminder.py +0 -0
- {frontengine-1.0.46/frontengine/utils/clipboard → frontengine-1.0.48/frontengine/utils/browser}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/browser/browser.py +0 -0
- {frontengine-1.0.46/frontengine/utils/color_vision → frontengine-1.0.48/frontengine/utils/clipboard}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/clipboard/clipboard_history.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/clipboard/clipboard_watcher.py +0 -0
- {frontengine-1.0.46/frontengine/utils/critical_exit → frontengine-1.0.48/frontengine/utils/color_vision}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/color_vision/color_vision.py +0 -0
- {frontengine-1.0.46/frontengine/utils/file → frontengine-1.0.48/frontengine/utils/critical_exit}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/critical_exit/check_key_is_press.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/critical_exit/critical_exit.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/critical_exit/win32_vk.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/exception/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/exception/exception_tags.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/exception/exceptions.py +0 -0
- {frontengine-1.0.46/frontengine/utils/focus_shield → frontengine-1.0.48/frontengine/utils/file}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/file/open/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/file/open/open_file.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/file/save/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/file/save/save_file.py +0 -0
- {frontengine-1.0.46/frontengine/utils/focus_timer → frontengine-1.0.48/frontengine/utils/focus_shield}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/focus_shield/focus_shield.py +0 -0
- {frontengine-1.0.46/frontengine/utils/hotkey → frontengine-1.0.48/frontengine/utils/focus_timer}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/focus_timer/focus_timer.py +0 -0
- {frontengine-1.0.46/frontengine/utils/input_watch → frontengine-1.0.48/frontengine/utils/hotkey}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/hotkey/hotkey_service.py +0 -0
- {frontengine-1.0.46/frontengine/utils/json → frontengine-1.0.48/frontengine/utils/input_watch}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/input_watch/input_watch_service.py +0 -0
- {frontengine-1.0.46/frontengine/utils/logging → frontengine-1.0.48/frontengine/utils/json}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/json/json_file.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/json/json_repository.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/json_format/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/json_format/json_process.py +0 -0
- {frontengine-1.0.46/frontengine/utils/measure → frontengine-1.0.48/frontengine/utils/logging}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/logging/loggin_instance.py +0 -0
- {frontengine-1.0.46/frontengine/utils/multi_language → frontengine-1.0.48/frontengine/utils/measure}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/measure/measure.py +0 -0
- {frontengine-1.0.46/frontengine/utils/now_playing → frontengine-1.0.48/frontengine/utils/midi}/__init__.py +0 -0
- {frontengine-1.0.46/frontengine/utils/obs → frontengine-1.0.48/frontengine/utils/multi_language}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/multi_language/france.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/multi_language/germany.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/multi_language/italy.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/multi_language/language_wrapper.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/multi_language/russian.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/multi_language/simplified_chinese.py +0 -0
- {frontengine-1.0.46/frontengine/utils/pet_chat → frontengine-1.0.48/frontengine/utils/now_playing}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/now_playing/now_playing.py +0 -0
- {frontengine-1.0.46/frontengine/utils/platform_info → frontengine-1.0.48/frontengine/utils/obs}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/obs/obs_client.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/obs/obs_protocol.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/obs/websocket_frames.py +0 -0
- {frontengine-1.0.46/frontengine/utils/playlist → frontengine-1.0.48/frontengine/utils/pet_chat}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/pet_chat/pet_chat_service.py +0 -0
- {frontengine-1.0.46/frontengine/utils/plugins → frontengine-1.0.48/frontengine/utils/platform_info}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/platform_info/platform_info.py +0 -0
- {frontengine-1.0.46/frontengine/utils/power_mode → frontengine-1.0.48/frontengine/utils/playlist}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/playlist/playlist.py +0 -0
- {frontengine-1.0.46/frontengine/utils/preset_schedule → frontengine-1.0.48/frontengine/utils/plugins}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/plugins/plugin_loader.py +0 -0
- {frontengine-1.0.46/frontengine/utils/recording → frontengine-1.0.48/frontengine/utils/power_mode}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/power_mode/power_mode.py +0 -0
- {frontengine-1.0.46/frontengine/utils/redirect_manager → frontengine-1.0.48/frontengine/utils/preset_schedule}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/preset_schedule/preset_schedule_service.py +0 -0
- {frontengine-1.0.46/frontengine/utils/reminder → frontengine-1.0.48/frontengine/utils/recording}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/recording/frame_recorder.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/recording/gif_writer.py +0 -0
- {frontengine-1.0.46/frontengine/utils/screen_privacy → frontengine-1.0.48/frontengine/utils/redirect_manager}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/redirect_manager/redirect_manager_class.py +0 -0
- {frontengine-1.0.46/frontengine/utils/screen_text → frontengine-1.0.48/frontengine/utils/reminder}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/reminder/reminder_service.py +0 -0
- {frontengine-1.0.46/frontengine/utils/smart_pause → frontengine-1.0.48/frontengine/utils/remote}/__init__.py +0 -0
- {frontengine-1.0.46/frontengine/utils/soundboard → frontengine-1.0.48/frontengine/utils/screen_privacy}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/screen_privacy/share_watch.py +0 -0
- {frontengine-1.0.46/frontengine/utils/system_stats → frontengine-1.0.48/frontengine/utils/screen_text}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/screen_text/screen_text_service.py +0 -0
- {frontengine-1.0.46/frontengine/utils/text_source → frontengine-1.0.48/frontengine/utils/signage}/__init__.py +0 -0
- {frontengine-1.0.46/frontengine/utils/theme_schedule → frontengine-1.0.48/frontengine/utils/smart_pause}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/smart_pause/pause_rules.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/smart_pause/smart_pause_service.py +0 -0
- {frontengine-1.0.46/frontengine/utils/usage_tracking → frontengine-1.0.48/frontengine/utils/soundboard}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/soundboard/soundboard.py +0 -0
- {frontengine-1.0.46/frontengine/utils/weather → frontengine-1.0.48/frontengine/utils/system_stats}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/system_stats/system_stats.py +0 -0
- {frontengine-1.0.46/frontengine/utils/window_pin → frontengine-1.0.48/frontengine/utils/text_source}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/text_source/text_source.py +0 -0
- {frontengine-1.0.46/frontengine/utils/workshop → frontengine-1.0.48/frontengine/utils/theme_schedule}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/theme_schedule/theme_schedule_service.py +0 -0
- {frontengine-1.0.46/frontengine/worker → frontengine-1.0.48/frontengine/utils/usage_tracking}/__init__.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/usage_tracking/usage_service.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/usage_tracking/usage_tracker.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/weather/weather_service.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/web_url.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/window_pin/window_layout.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/window_pin/window_pin.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/utils/workshop/workshop_content.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine/worker/qthread_worker.py +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine.egg-info/dependency_links.txt +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine.egg-info/entry_points.txt +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine.egg-info/requires.txt +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/frontengine.egg-info/top_level.txt +0 -0
- {frontengine-1.0.46 → frontengine-1.0.48}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: frontengine
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.48
|
|
4
4
|
Summary: FrontEngine is use-define frontview or only use like screen saver
|
|
5
5
|
Author-email: JE-Chen <jechenmailman@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/Intergration-Automation-Testing/FrontEngine
|
|
@@ -143,6 +143,14 @@ Measuring and capture tools on the **Tools** tab:
|
|
|
143
143
|
`hsl(...)` or a CSS custom property.
|
|
144
144
|
- **Region capture** — drag out an area; it lands on the clipboard and can be
|
|
145
145
|
saved to a file.
|
|
146
|
+
- **Virtual camera** — drag out an area and send it, overlays and all, as a
|
|
147
|
+
webcam that Zoom, Teams or Discord can pick as their video source. Needs the
|
|
148
|
+
optional `pyvirtualcam` package and a virtual camera driver on the system
|
|
149
|
+
(OBS installs one); without either, the button says so rather than failing
|
|
150
|
+
quietly.
|
|
151
|
+
- **Camera** — any video input works, including capture cards, and the device
|
|
152
|
+
list can be refreshed without restarting since cards are usually plugged in
|
|
153
|
+
while the app is already running.
|
|
146
154
|
- **Read text** — drag out an area to copy the text in it, translate it, or ask
|
|
147
155
|
a question about it.
|
|
148
156
|
> This is the **only** feature here that sends screen content off the
|
|
@@ -196,6 +204,42 @@ Four things on the **Stream** tab, for playing and for going live:
|
|
|
196
204
|
|
|
197
205
|
---
|
|
198
206
|
|
|
207
|
+
## Signage, whiteboard and lip-sync
|
|
208
|
+
|
|
209
|
+
- **Signage mode** (Settings) — rotate through a list of presets on a timer and
|
|
210
|
+
put the main window away, for a machine left running as a display. The
|
|
211
|
+
rotation wraps, so it keeps cycling.
|
|
212
|
+
- **Whiteboard** (Presenting tab) — an infinite canvas: drag with the middle
|
|
213
|
+
button to pan, scroll to zoom, and save what you drew as an image. Strokes
|
|
214
|
+
live in canvas coordinates, so panning and zooming leaves them where they
|
|
215
|
+
belong.
|
|
216
|
+
- **Pet lip-sync** — the pet can react to your **microphone** instead of your
|
|
217
|
+
speakers, so it moves while you talk. Like the speaker reaction this reads
|
|
218
|
+
only the meter — a number, not sound. Windows only.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Remote control
|
|
223
|
+
|
|
224
|
+
From **Settings → Remote control**:
|
|
225
|
+
|
|
226
|
+
- **Your phone** — turn it on and FrontEngine serves a small page on your local
|
|
227
|
+
network; open the link on a phone and the buttons drive the same actions the
|
|
228
|
+
global hotkeys do.
|
|
229
|
+
> This opens a port on your machine, so it is **off by default**. The link
|
|
230
|
+
> carries a token that is regenerated every time the server starts, so an old
|
|
231
|
+
> link stops working, and the page can only ask for the fixed action list —
|
|
232
|
+
> nothing else is reachable. It is plain HTTP: someone else on the same
|
|
233
|
+
> network could read the token and press the same buttons, which is a
|
|
234
|
+
> nuisance rather than a breach given what the buttons do, but leave it off
|
|
235
|
+
> on networks you do not trust.
|
|
236
|
+
- **A MIDI controller** — press *Learn*, move a knob or pad, and bind it to an
|
|
237
|
+
action. It uses Windows' built-in winmm, so no extra package is needed. A
|
|
238
|
+
knob fires once it reaches the top rather than repeatedly on the way, and
|
|
239
|
+
releasing a pad does not count as a second press.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
199
243
|
## Screen-sharing privacy
|
|
200
244
|
|
|
201
245
|
Your overlays are for you, not for the people you are sharing with. From
|
|
@@ -119,6 +119,14 @@ Measuring and capture tools on the **Tools** tab:
|
|
|
119
119
|
`hsl(...)` or a CSS custom property.
|
|
120
120
|
- **Region capture** — drag out an area; it lands on the clipboard and can be
|
|
121
121
|
saved to a file.
|
|
122
|
+
- **Virtual camera** — drag out an area and send it, overlays and all, as a
|
|
123
|
+
webcam that Zoom, Teams or Discord can pick as their video source. Needs the
|
|
124
|
+
optional `pyvirtualcam` package and a virtual camera driver on the system
|
|
125
|
+
(OBS installs one); without either, the button says so rather than failing
|
|
126
|
+
quietly.
|
|
127
|
+
- **Camera** — any video input works, including capture cards, and the device
|
|
128
|
+
list can be refreshed without restarting since cards are usually plugged in
|
|
129
|
+
while the app is already running.
|
|
122
130
|
- **Read text** — drag out an area to copy the text in it, translate it, or ask
|
|
123
131
|
a question about it.
|
|
124
132
|
> This is the **only** feature here that sends screen content off the
|
|
@@ -172,6 +180,42 @@ Four things on the **Stream** tab, for playing and for going live:
|
|
|
172
180
|
|
|
173
181
|
---
|
|
174
182
|
|
|
183
|
+
## Signage, whiteboard and lip-sync
|
|
184
|
+
|
|
185
|
+
- **Signage mode** (Settings) — rotate through a list of presets on a timer and
|
|
186
|
+
put the main window away, for a machine left running as a display. The
|
|
187
|
+
rotation wraps, so it keeps cycling.
|
|
188
|
+
- **Whiteboard** (Presenting tab) — an infinite canvas: drag with the middle
|
|
189
|
+
button to pan, scroll to zoom, and save what you drew as an image. Strokes
|
|
190
|
+
live in canvas coordinates, so panning and zooming leaves them where they
|
|
191
|
+
belong.
|
|
192
|
+
- **Pet lip-sync** — the pet can react to your **microphone** instead of your
|
|
193
|
+
speakers, so it moves while you talk. Like the speaker reaction this reads
|
|
194
|
+
only the meter — a number, not sound. Windows only.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Remote control
|
|
199
|
+
|
|
200
|
+
From **Settings → Remote control**:
|
|
201
|
+
|
|
202
|
+
- **Your phone** — turn it on and FrontEngine serves a small page on your local
|
|
203
|
+
network; open the link on a phone and the buttons drive the same actions the
|
|
204
|
+
global hotkeys do.
|
|
205
|
+
> This opens a port on your machine, so it is **off by default**. The link
|
|
206
|
+
> carries a token that is regenerated every time the server starts, so an old
|
|
207
|
+
> link stops working, and the page can only ask for the fixed action list —
|
|
208
|
+
> nothing else is reachable. It is plain HTTP: someone else on the same
|
|
209
|
+
> network could read the token and press the same buttons, which is a
|
|
210
|
+
> nuisance rather than a breach given what the buttons do, but leave it off
|
|
211
|
+
> on networks you do not trust.
|
|
212
|
+
- **A MIDI controller** — press *Learn*, move a knob or pad, and bind it to an
|
|
213
|
+
action. It uses Windows' built-in winmm, so no extra package is needed. A
|
|
214
|
+
knob fires once it reaches the top rather than repeatedly on the way, and
|
|
215
|
+
releasing a pad does not count as a second press.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
175
219
|
## Screen-sharing privacy
|
|
176
220
|
|
|
177
221
|
Your overlays are for you, not for the people you are sharing with. From
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""
|
|
2
|
+
無限畫布白板:可以一直往外畫、拖曳平移、滾輪縮放,最後存成圖片。
|
|
3
|
+
|
|
4
|
+
和簡報分頁的塗鴉層不同的是,筆畫存的是「畫布座標」而不是螢幕座標,所以平移
|
|
5
|
+
縮放之後線條還在它原本該在的地方——這正是「無限」的意思。
|
|
6
|
+
|
|
7
|
+
An infinite whiteboard: keep drawing outwards, drag to pan, scroll to zoom, and
|
|
8
|
+
save the result as an image.
|
|
9
|
+
|
|
10
|
+
Unlike the presenting page's annotation layer, strokes are stored in canvas
|
|
11
|
+
coordinates rather than screen ones, so panning and zooming leaves them where
|
|
12
|
+
they belong - which is what makes it infinite.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
18
|
+
|
|
19
|
+
from PySide6.QtCore import QPoint, QPointF, QRectF, Qt
|
|
20
|
+
from PySide6.QtGui import QColor, QImage, QPainter, QPen
|
|
21
|
+
|
|
22
|
+
from frontengine.show.base_widget import BaseWidget
|
|
23
|
+
from frontengine.show.window_helpers import apply_overlay_window_flags
|
|
24
|
+
from frontengine.utils.logging.loggin_instance import front_engine_logger
|
|
25
|
+
|
|
26
|
+
DEFAULT_COLOR = "#ffd54f"
|
|
27
|
+
DEFAULT_WIDTH = 4
|
|
28
|
+
MIN_ZOOM = 0.2
|
|
29
|
+
MAX_ZOOM = 5.0
|
|
30
|
+
ZOOM_STEP = 1.15
|
|
31
|
+
_SAVE_MARGIN = 24
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def clamp_zoom(value: Any, fallback: float = 1.0) -> float:
|
|
35
|
+
"""縮放倍率夾在看得清楚又不會迷路的範圍。"""
|
|
36
|
+
try:
|
|
37
|
+
return max(MIN_ZOOM, min(MAX_ZOOM, float(value)))
|
|
38
|
+
except (TypeError, ValueError):
|
|
39
|
+
return fallback
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def to_canvas(point: Tuple[float, float], offset: Tuple[float, float], zoom: float) -> Tuple[float, float]:
|
|
43
|
+
"""螢幕座標 -> 畫布座標。"""
|
|
44
|
+
scale = clamp_zoom(zoom)
|
|
45
|
+
return ((point[0] - offset[0]) / scale, (point[1] - offset[1]) / scale)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def to_screen(point: Tuple[float, float], offset: Tuple[float, float], zoom: float) -> Tuple[float, float]:
|
|
49
|
+
"""畫布座標 -> 螢幕座標。"""
|
|
50
|
+
scale = clamp_zoom(zoom)
|
|
51
|
+
return (point[0] * scale + offset[0], point[1] * scale + offset[1])
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def content_bounds(strokes: List[Dict[str, Any]]) -> Optional[Tuple[float, float, float, float]]:
|
|
55
|
+
"""
|
|
56
|
+
所有筆畫涵蓋的畫布範圍 (left, top, right, bottom);沒有筆畫回傳 None。
|
|
57
|
+
存檔時用它決定要輸出多大,才不會存出一大片空白。
|
|
58
|
+
The canvas area the strokes cover, or None when there are none. Saving uses
|
|
59
|
+
it to size the image, so the result is not mostly blank.
|
|
60
|
+
"""
|
|
61
|
+
points = [point for stroke in strokes for point in stroke.get("points", [])]
|
|
62
|
+
if not points:
|
|
63
|
+
return None
|
|
64
|
+
xs = [point[0] for point in points]
|
|
65
|
+
ys = [point[1] for point in points]
|
|
66
|
+
return (min(xs), min(ys), max(xs), max(ys))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class WhiteboardWidget(BaseWidget):
|
|
70
|
+
"""
|
|
71
|
+
無限畫布。筆畫以畫布座標保存,畫的時候才換算成螢幕座標。
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, color: str = DEFAULT_COLOR, width: int = DEFAULT_WIDTH) -> None:
|
|
75
|
+
front_engine_logger.info("[WhiteboardWidget] Init")
|
|
76
|
+
super().__init__()
|
|
77
|
+
candidate = QColor(color)
|
|
78
|
+
self.color = candidate if candidate.isValid() else QColor(DEFAULT_COLOR)
|
|
79
|
+
self.pen_width = max(1, int(width))
|
|
80
|
+
self.opacity = 1.0
|
|
81
|
+
self.overlay_lockable = False
|
|
82
|
+
self.strokes: List[Dict[str, Any]] = []
|
|
83
|
+
self.zoom = 1.0
|
|
84
|
+
self.offset = [0.0, 0.0]
|
|
85
|
+
self._stroke: Optional[Dict[str, Any]] = None
|
|
86
|
+
self._pan_origin: Optional[QPoint] = None
|
|
87
|
+
apply_overlay_window_flags(self, show_on_bottom=False, allow_input=True)
|
|
88
|
+
|
|
89
|
+
# --- drawing ---------------------------------------------------------
|
|
90
|
+
def begin_stroke(self, point: QPoint) -> None:
|
|
91
|
+
"""開始一筆。"""
|
|
92
|
+
self._stroke = {"color": self.color.name(), "width": self.pen_width,
|
|
93
|
+
"points": [to_canvas((point.x(), point.y()), self.offset, self.zoom)]}
|
|
94
|
+
|
|
95
|
+
def extend_stroke(self, point: QPoint) -> None:
|
|
96
|
+
if self._stroke is not None:
|
|
97
|
+
self._stroke["points"].append(to_canvas((point.x(), point.y()), self.offset, self.zoom))
|
|
98
|
+
self.update()
|
|
99
|
+
|
|
100
|
+
def end_stroke(self) -> bool:
|
|
101
|
+
"""
|
|
102
|
+
收筆。只有一個點的「筆畫」是誤點,丟掉不留。
|
|
103
|
+
Finish. A one-point stroke is a stray click and is discarded.
|
|
104
|
+
"""
|
|
105
|
+
stroke, self._stroke = self._stroke, None
|
|
106
|
+
if stroke is None or len(stroke["points"]) < 2:
|
|
107
|
+
return False
|
|
108
|
+
self.strokes.append(stroke)
|
|
109
|
+
self.update()
|
|
110
|
+
return True
|
|
111
|
+
|
|
112
|
+
def undo(self) -> bool:
|
|
113
|
+
"""收回最後一筆。"""
|
|
114
|
+
if not self.strokes:
|
|
115
|
+
return False
|
|
116
|
+
self.strokes.pop()
|
|
117
|
+
self.update()
|
|
118
|
+
return True
|
|
119
|
+
|
|
120
|
+
def clear(self) -> None:
|
|
121
|
+
self.strokes = []
|
|
122
|
+
self._stroke = None
|
|
123
|
+
self.update()
|
|
124
|
+
|
|
125
|
+
def set_pen(self, color: Optional[str] = None, width: Optional[int] = None) -> None:
|
|
126
|
+
if color is not None:
|
|
127
|
+
candidate = QColor(color)
|
|
128
|
+
if candidate.isValid():
|
|
129
|
+
self.color = candidate
|
|
130
|
+
if width is not None:
|
|
131
|
+
self.pen_width = max(1, int(width))
|
|
132
|
+
|
|
133
|
+
# --- navigating ------------------------------------------------------
|
|
134
|
+
def pan(self, dx: float, dy: float) -> None:
|
|
135
|
+
"""平移畫布。"""
|
|
136
|
+
self.offset[0] += float(dx)
|
|
137
|
+
self.offset[1] += float(dy)
|
|
138
|
+
self.update()
|
|
139
|
+
|
|
140
|
+
def zoom_at(self, point: QPoint, factor: float) -> float:
|
|
141
|
+
"""
|
|
142
|
+
以游標為中心縮放:游標底下的那一點在縮放前後要停在原地,
|
|
143
|
+
不然放大時畫面會整個跑掉。
|
|
144
|
+
Zoom around the cursor so whatever is under it stays put; otherwise the
|
|
145
|
+
view lurches away every time you zoom.
|
|
146
|
+
"""
|
|
147
|
+
before = to_canvas((point.x(), point.y()), self.offset, self.zoom)
|
|
148
|
+
self.zoom = clamp_zoom(self.zoom * float(factor))
|
|
149
|
+
after = to_screen(before, self.offset, self.zoom)
|
|
150
|
+
self.offset[0] += point.x() - after[0]
|
|
151
|
+
self.offset[1] += point.y() - after[1]
|
|
152
|
+
self.update()
|
|
153
|
+
return self.zoom
|
|
154
|
+
|
|
155
|
+
def reset_view(self) -> None:
|
|
156
|
+
"""回到原點與原始比例(筆畫都還在)。"""
|
|
157
|
+
self.zoom = 1.0
|
|
158
|
+
self.offset = [0.0, 0.0]
|
|
159
|
+
self.update()
|
|
160
|
+
|
|
161
|
+
# --- events ----------------------------------------------------------
|
|
162
|
+
def mousePressEvent(self, event) -> None:
|
|
163
|
+
if event.button() == Qt.MouseButton.LeftButton:
|
|
164
|
+
self.begin_stroke(event.position().toPoint())
|
|
165
|
+
elif event.button() == Qt.MouseButton.MiddleButton:
|
|
166
|
+
self._pan_origin = event.position().toPoint()
|
|
167
|
+
super().mousePressEvent(event)
|
|
168
|
+
|
|
169
|
+
def mouseMoveEvent(self, event) -> None:
|
|
170
|
+
point = event.position().toPoint()
|
|
171
|
+
if self._pan_origin is not None:
|
|
172
|
+
self.pan(point.x() - self._pan_origin.x(), point.y() - self._pan_origin.y())
|
|
173
|
+
self._pan_origin = point
|
|
174
|
+
else:
|
|
175
|
+
self.extend_stroke(point)
|
|
176
|
+
super().mouseMoveEvent(event)
|
|
177
|
+
|
|
178
|
+
def mouseReleaseEvent(self, event) -> None:
|
|
179
|
+
if event.button() == Qt.MouseButton.MiddleButton:
|
|
180
|
+
self._pan_origin = None
|
|
181
|
+
else:
|
|
182
|
+
self.end_stroke()
|
|
183
|
+
super().mouseReleaseEvent(event)
|
|
184
|
+
|
|
185
|
+
def wheelEvent(self, event) -> None:
|
|
186
|
+
step = ZOOM_STEP if event.angleDelta().y() > 0 else 1.0 / ZOOM_STEP
|
|
187
|
+
self.zoom_at(event.position().toPoint(), step)
|
|
188
|
+
|
|
189
|
+
# --- output ----------------------------------------------------------
|
|
190
|
+
def save_image(self, path: str) -> Optional[str]:
|
|
191
|
+
"""
|
|
192
|
+
把所有筆畫存成一張圖(只涵蓋畫過的範圍)。沒有筆畫就不存,回傳 None。
|
|
193
|
+
Save the strokes as an image covering only the area drawn on. Nothing
|
|
194
|
+
drawn means nothing saved.
|
|
195
|
+
"""
|
|
196
|
+
bounds = content_bounds(self.strokes)
|
|
197
|
+
if bounds is None:
|
|
198
|
+
return None
|
|
199
|
+
left, top, right, bottom = bounds
|
|
200
|
+
width = int(right - left) + _SAVE_MARGIN * 2
|
|
201
|
+
height = int(bottom - top) + _SAVE_MARGIN * 2
|
|
202
|
+
image = QImage(max(1, width), max(1, height), QImage.Format.Format_ARGB32)
|
|
203
|
+
image.fill(Qt.GlobalColor.transparent)
|
|
204
|
+
painter = QPainter(image)
|
|
205
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
|
|
206
|
+
for stroke in self.strokes:
|
|
207
|
+
self._paint_stroke(painter, stroke,
|
|
208
|
+
offset=(_SAVE_MARGIN - left, _SAVE_MARGIN - top), zoom=1.0)
|
|
209
|
+
painter.end()
|
|
210
|
+
target = Path(path)
|
|
211
|
+
try:
|
|
212
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
213
|
+
if not image.save(str(target)):
|
|
214
|
+
return None
|
|
215
|
+
except OSError as error:
|
|
216
|
+
front_engine_logger.warning(f"[WhiteboardWidget] save failed: {error!r}")
|
|
217
|
+
return None
|
|
218
|
+
front_engine_logger.info(f"[WhiteboardWidget] saved | {target}")
|
|
219
|
+
return str(target)
|
|
220
|
+
|
|
221
|
+
def _paint_stroke(self, painter: QPainter, stroke: Dict[str, Any],
|
|
222
|
+
offset, zoom: float) -> None:
|
|
223
|
+
points = stroke.get("points", [])
|
|
224
|
+
if len(points) < 2:
|
|
225
|
+
return
|
|
226
|
+
pen = QPen(QColor(stroke.get("color", DEFAULT_COLOR)),
|
|
227
|
+
max(1.0, float(stroke.get("width", DEFAULT_WIDTH)) * zoom))
|
|
228
|
+
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
|
229
|
+
pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
|
|
230
|
+
painter.setPen(pen)
|
|
231
|
+
previous = to_screen(points[0], offset, zoom)
|
|
232
|
+
for point in points[1:]:
|
|
233
|
+
current = to_screen(point, offset, zoom)
|
|
234
|
+
painter.drawLine(QPointF(*previous), QPointF(*current))
|
|
235
|
+
previous = current
|
|
236
|
+
|
|
237
|
+
def draw_content(self, painter: QPainter) -> None:
|
|
238
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
|
|
239
|
+
painter.fillRect(QRectF(self.rect()), QColor(0, 0, 0, 120))
|
|
240
|
+
for stroke in self.strokes:
|
|
241
|
+
self._paint_stroke(painter, stroke, self.offset, self.zoom)
|
|
242
|
+
if self._stroke is not None:
|
|
243
|
+
self._paint_stroke(painter, self._stroke, self.offset, self.zoom)
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""
|
|
2
|
+
遙控設定:手機網頁遙控與 MIDI 控制器。
|
|
3
|
+
|
|
4
|
+
手機遙控會在這台機器上開一個網路埠,所以預設關閉、每次啟動換新權杖、
|
|
5
|
+
只接受固定的動作清單——這些在畫面上都寫清楚,不藏在文件裡。
|
|
6
|
+
|
|
7
|
+
Remote control settings: the phone web remote and a MIDI controller.
|
|
8
|
+
|
|
9
|
+
The phone remote opens a network port on this machine, so it is off by default,
|
|
10
|
+
mints a fresh token at every start and only accepts a fixed list of actions -
|
|
11
|
+
all of which the dialog says out loud rather than hiding in the docs.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any, Dict, Optional
|
|
16
|
+
|
|
17
|
+
from PySide6.QtGui import QGuiApplication
|
|
18
|
+
from PySide6.QtWidgets import (
|
|
19
|
+
QAbstractItemView, QCheckBox, QComboBox, QDialog, QDialogButtonBox, QGridLayout, QLabel,
|
|
20
|
+
QPushButton, QTableWidget, QTableWidgetItem, QWidget,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from frontengine.user_setting.user_setting_file import user_setting_dict, write_user_setting
|
|
24
|
+
from frontengine.utils.logging.loggin_instance import front_engine_logger
|
|
25
|
+
from frontengine.utils.midi.midi_input import available as midi_available, list_devices
|
|
26
|
+
from frontengine.utils.multi_language.language_wrapper import language_wrapper
|
|
27
|
+
from frontengine.utils.remote.remote_server import ALLOWED_ACTIONS
|
|
28
|
+
|
|
29
|
+
MIDI_KEY = "midi_bindings"
|
|
30
|
+
REMOTE_KEY = "phone_remote"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _t(key: str, fallback: str) -> str:
|
|
34
|
+
return language_wrapper.language_word_dict.get(key, fallback)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def current_bindings() -> Dict[str, str]:
|
|
38
|
+
"""設定裡的 {MIDI 鍵: 動作},只留允許清單裡的動作。"""
|
|
39
|
+
stored = user_setting_dict.get(MIDI_KEY)
|
|
40
|
+
if not isinstance(stored, dict):
|
|
41
|
+
return {}
|
|
42
|
+
return {str(key): str(value) for key, value in stored.items()
|
|
43
|
+
if str(value) in ALLOWED_ACTIONS}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def remote_enabled() -> bool:
|
|
47
|
+
settings = user_setting_dict.get(REMOTE_KEY)
|
|
48
|
+
return bool(settings.get("enabled")) if isinstance(settings, dict) else False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class RemoteControlDialog(QDialog):
|
|
52
|
+
"""設定手機遙控與 MIDI 綁定。"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, parent: Optional[QWidget] = None, remote=None, midi=None) -> None:
|
|
55
|
+
front_engine_logger.info("[RemoteControlDialog] Init")
|
|
56
|
+
super().__init__(parent)
|
|
57
|
+
self.setWindowTitle(_t("remote_title", "Remote control"))
|
|
58
|
+
self._remote = remote
|
|
59
|
+
self._midi = midi
|
|
60
|
+
self._learning = False
|
|
61
|
+
|
|
62
|
+
self.remote_checkbox = QCheckBox(_t("remote_enable", "Let my phone control FrontEngine"))
|
|
63
|
+
self.remote_checkbox.setChecked(remote_enabled())
|
|
64
|
+
self.remote_checkbox.toggled.connect(self._toggle_remote)
|
|
65
|
+
self.remote_url_label = QLabel(self.remote_status())
|
|
66
|
+
self.remote_url_label.setWordWrap(True)
|
|
67
|
+
self.remote_url_label.setTextInteractionFlags(
|
|
68
|
+
self.remote_url_label.textInteractionFlags().TextSelectableByMouse)
|
|
69
|
+
self.remote_copy_button = QPushButton(_t("remote_copy", "Copy link"))
|
|
70
|
+
self.remote_copy_button.clicked.connect(self.copy_link)
|
|
71
|
+
self.remote_hint = QLabel(
|
|
72
|
+
_t("remote_hint",
|
|
73
|
+
"This opens a port on this machine for your local network. The link carries a "
|
|
74
|
+
"one-time token that changes every time it starts, and only the buttons on the "
|
|
75
|
+
"page can be triggered - nothing else. It is plain HTTP, so treat it like any "
|
|
76
|
+
"other device on your network: someone else on the same network could read the "
|
|
77
|
+
"token and press the same buttons. Leave it off on networks you do not trust."))
|
|
78
|
+
self.remote_hint.setWordWrap(True)
|
|
79
|
+
|
|
80
|
+
self.midi_label = QLabel(_t("remote_midi_label", "MIDI controller"))
|
|
81
|
+
self.midi_combobox = QComboBox()
|
|
82
|
+
for index, name in list_devices():
|
|
83
|
+
self.midi_combobox.addItem(name, index)
|
|
84
|
+
if self.midi_combobox.count() == 0:
|
|
85
|
+
self.midi_combobox.addItem(_t("remote_midi_none", "No MIDI device found"), -1)
|
|
86
|
+
self.learn_button = QPushButton(_t("remote_midi_learn", "Learn"))
|
|
87
|
+
self.learn_button.clicked.connect(self.start_learning)
|
|
88
|
+
self.learn_button.setEnabled(midi_available())
|
|
89
|
+
self.remove_button = QPushButton(_t("remote_midi_remove", "Remove"))
|
|
90
|
+
self.remove_button.clicked.connect(self.remove_selected)
|
|
91
|
+
|
|
92
|
+
self.table = QTableWidget(0, 2)
|
|
93
|
+
self.table.setHorizontalHeaderLabels([
|
|
94
|
+
_t("remote_midi_control_column", "Control"),
|
|
95
|
+
_t("remote_midi_action_column", "Action"),
|
|
96
|
+
])
|
|
97
|
+
self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
|
98
|
+
self.table.horizontalHeader().setStretchLastSection(True)
|
|
99
|
+
|
|
100
|
+
self.button_box = QDialogButtonBox(
|
|
101
|
+
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
|
102
|
+
self.button_box.accepted.connect(self.accept)
|
|
103
|
+
self.button_box.rejected.connect(self.reject)
|
|
104
|
+
|
|
105
|
+
layout = QGridLayout(self)
|
|
106
|
+
layout.addWidget(self.remote_checkbox, 0, 0, 1, 3)
|
|
107
|
+
layout.addWidget(self.remote_url_label, 1, 0, 1, 2)
|
|
108
|
+
layout.addWidget(self.remote_copy_button, 1, 2)
|
|
109
|
+
layout.addWidget(self.remote_hint, 2, 0, 1, 3)
|
|
110
|
+
layout.addWidget(self.midi_label, 3, 0)
|
|
111
|
+
layout.addWidget(self.midi_combobox, 3, 1, 1, 2)
|
|
112
|
+
layout.addWidget(self.table, 4, 0, 1, 3)
|
|
113
|
+
layout.addWidget(self.learn_button, 5, 0)
|
|
114
|
+
layout.addWidget(self.remove_button, 5, 1)
|
|
115
|
+
layout.addWidget(self.button_box, 6, 0, 1, 3)
|
|
116
|
+
|
|
117
|
+
for control, action in current_bindings().items():
|
|
118
|
+
self.add_row(control, action)
|
|
119
|
+
|
|
120
|
+
# --- phone remote ----------------------------------------------------
|
|
121
|
+
def remote_status(self) -> str:
|
|
122
|
+
"""目前該在手機上開什麼;沒開啟就說明它是關著的。"""
|
|
123
|
+
if self._remote is None or not getattr(self._remote, "running", False):
|
|
124
|
+
return _t("remote_off", "Off - nothing is listening.")
|
|
125
|
+
return self._remote.url()
|
|
126
|
+
|
|
127
|
+
def copy_link(self) -> bool:
|
|
128
|
+
"""把連結複製起來,方便貼到手機上。"""
|
|
129
|
+
if self._remote is None or not getattr(self._remote, "running", False):
|
|
130
|
+
return False
|
|
131
|
+
clipboard = QGuiApplication.clipboard()
|
|
132
|
+
if clipboard is None: # pragma: no cover - no clipboard at all
|
|
133
|
+
return False
|
|
134
|
+
clipboard.setText(self._remote.url())
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
def _toggle_remote(self, enabled: bool) -> None:
|
|
138
|
+
user_setting_dict[REMOTE_KEY] = {"enabled": bool(enabled)}
|
|
139
|
+
write_user_setting()
|
|
140
|
+
if self._remote is None:
|
|
141
|
+
return
|
|
142
|
+
if enabled:
|
|
143
|
+
self._remote.start()
|
|
144
|
+
else:
|
|
145
|
+
self._remote.stop()
|
|
146
|
+
self.remote_url_label.setText(self.remote_status())
|
|
147
|
+
|
|
148
|
+
# --- MIDI bindings ---------------------------------------------------
|
|
149
|
+
def add_row(self, control: str = "", action: str = "") -> None:
|
|
150
|
+
"""加一列綁定。"""
|
|
151
|
+
row = self.table.rowCount()
|
|
152
|
+
self.table.insertRow(row)
|
|
153
|
+
self.table.setItem(row, 0, QTableWidgetItem(str(control)))
|
|
154
|
+
combobox = QComboBox()
|
|
155
|
+
combobox.addItems(list(ALLOWED_ACTIONS))
|
|
156
|
+
if action in ALLOWED_ACTIONS:
|
|
157
|
+
combobox.setCurrentText(action)
|
|
158
|
+
self.table.setCellWidget(row, 1, combobox)
|
|
159
|
+
|
|
160
|
+
def start_learning(self) -> None:
|
|
161
|
+
"""按下之後,下一個收到的 MIDI 訊息就會變成新的一列。"""
|
|
162
|
+
self._learning = True
|
|
163
|
+
self.learn_button.setText(_t("remote_midi_listening", "Press a control..."))
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def learning(self) -> bool:
|
|
167
|
+
return self._learning
|
|
168
|
+
|
|
169
|
+
def learn(self, control: str) -> bool:
|
|
170
|
+
"""收到 MIDI 訊息時由主視窗呼叫;沒在學習狀態就忽略。"""
|
|
171
|
+
if not self._learning:
|
|
172
|
+
return False
|
|
173
|
+
self._learning = False
|
|
174
|
+
self.learn_button.setText(_t("remote_midi_learn", "Learn"))
|
|
175
|
+
self.add_row(control, ALLOWED_ACTIONS[0])
|
|
176
|
+
return True
|
|
177
|
+
|
|
178
|
+
def remove_selected(self) -> bool:
|
|
179
|
+
row = self.table.currentRow()
|
|
180
|
+
if row < 0:
|
|
181
|
+
return False
|
|
182
|
+
self.table.removeRow(row)
|
|
183
|
+
return True
|
|
184
|
+
|
|
185
|
+
def bindings(self) -> Dict[str, str]:
|
|
186
|
+
"""目前表格上的綁定。"""
|
|
187
|
+
bindings: Dict[str, str] = {}
|
|
188
|
+
for row in range(self.table.rowCount()):
|
|
189
|
+
item = self.table.item(row, 0)
|
|
190
|
+
combobox = self.table.cellWidget(row, 1)
|
|
191
|
+
control = item.text().strip() if item else ""
|
|
192
|
+
action = combobox.currentText() if combobox else ""
|
|
193
|
+
if control and action in ALLOWED_ACTIONS:
|
|
194
|
+
bindings[control] = action
|
|
195
|
+
return bindings
|
|
196
|
+
|
|
197
|
+
def midi_device(self) -> Any:
|
|
198
|
+
"""選到的 MIDI 裝置索引。"""
|
|
199
|
+
return self.midi_combobox.currentData()
|
|
200
|
+
|
|
201
|
+
def accept(self) -> None:
|
|
202
|
+
bindings = self.bindings()
|
|
203
|
+
user_setting_dict[MIDI_KEY] = bindings
|
|
204
|
+
user_setting_dict["midi_device"] = self.midi_device()
|
|
205
|
+
write_user_setting()
|
|
206
|
+
front_engine_logger.info(f"[RemoteControlDialog] saved | {len(bindings)} binding(s)")
|
|
207
|
+
super().accept()
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
看板模式設定:依序輪播一組預設集,適合把機器擺著當展示螢幕。
|
|
3
|
+
|
|
4
|
+
Signage settings: rotate through a set of presets, for a machine left running
|
|
5
|
+
as a display.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
|
|
11
|
+
from PySide6.QtWidgets import (
|
|
12
|
+
QCheckBox, QDialog, QDialogButtonBox, QGridLayout, QLabel, QPlainTextEdit, QSpinBox, QWidget,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from frontengine.user_setting.user_setting_file import user_setting_dict, write_user_setting
|
|
16
|
+
from frontengine.utils.logging.loggin_instance import front_engine_logger
|
|
17
|
+
from frontengine.utils.multi_language.language_wrapper import language_wrapper
|
|
18
|
+
from frontengine.utils.signage.signage_service import (
|
|
19
|
+
DEFAULT_INTERVAL_MINUTES, MAX_INTERVAL_MINUTES, MIN_INTERVAL_MINUTES, clamp_interval,
|
|
20
|
+
normalize_playlist,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
SETTING_KEY = "signage"
|
|
24
|
+
DEFAULT_SETTINGS: Dict[str, Any] = {
|
|
25
|
+
"enabled": False, "presets": [], "interval_minutes": DEFAULT_INTERVAL_MINUTES,
|
|
26
|
+
"hide_window": True,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _t(key: str, fallback: str) -> str:
|
|
31
|
+
return language_wrapper.language_word_dict.get(key, fallback)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def current_settings() -> Dict[str, Any]:
|
|
35
|
+
"""目前的看板設定(缺項目補預設值)。"""
|
|
36
|
+
stored = user_setting_dict.get(SETTING_KEY)
|
|
37
|
+
settings = dict(DEFAULT_SETTINGS)
|
|
38
|
+
if isinstance(stored, dict):
|
|
39
|
+
settings.update({key: stored[key] for key in DEFAULT_SETTINGS if key in stored})
|
|
40
|
+
settings["presets"] = normalize_playlist(settings.get("presets"))
|
|
41
|
+
settings["interval_minutes"] = clamp_interval(settings.get("interval_minutes"))
|
|
42
|
+
return settings
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SignageDialog(QDialog):
|
|
46
|
+
"""設定要輪播哪些預設集、多久換一次。"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, parent: Optional[QWidget] = None) -> None:
|
|
49
|
+
front_engine_logger.info("[SignageDialog] Init")
|
|
50
|
+
super().__init__(parent)
|
|
51
|
+
self.setWindowTitle(_t("signage_title", "Signage mode"))
|
|
52
|
+
settings = current_settings()
|
|
53
|
+
|
|
54
|
+
self.enable_checkbox = QCheckBox(_t("signage_enable", "Rotate presets"))
|
|
55
|
+
self.enable_checkbox.setChecked(bool(settings.get("enabled")))
|
|
56
|
+
self.presets_label = QLabel(_t("signage_presets_label", "Presets, one per line:"))
|
|
57
|
+
self.presets_edit = QPlainTextEdit("\n".join(settings.get("presets", [])))
|
|
58
|
+
self.interval_label = QLabel(_t("signage_interval_label", "Change every (minutes)"))
|
|
59
|
+
self.interval_spinbox = QSpinBox()
|
|
60
|
+
self.interval_spinbox.setRange(MIN_INTERVAL_MINUTES, MAX_INTERVAL_MINUTES)
|
|
61
|
+
self.interval_spinbox.setValue(settings.get("interval_minutes"))
|
|
62
|
+
self.hide_checkbox = QCheckBox(
|
|
63
|
+
_t("signage_hide_window", "Hide the main window while rotating"))
|
|
64
|
+
self.hide_checkbox.setChecked(bool(settings.get("hide_window", True)))
|
|
65
|
+
self.hint_label = QLabel(
|
|
66
|
+
_t("signage_hint",
|
|
67
|
+
"Presets are saved from the Presets menu. The rotation wraps at the end, so a "
|
|
68
|
+
"machine left running keeps cycling."))
|
|
69
|
+
self.hint_label.setWordWrap(True)
|
|
70
|
+
|
|
71
|
+
self.button_box = QDialogButtonBox(
|
|
72
|
+
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
|
73
|
+
self.button_box.accepted.connect(self.accept)
|
|
74
|
+
self.button_box.rejected.connect(self.reject)
|
|
75
|
+
|
|
76
|
+
layout = QGridLayout(self)
|
|
77
|
+
layout.addWidget(self.enable_checkbox, 0, 0, 1, 2)
|
|
78
|
+
layout.addWidget(self.presets_label, 1, 0, 1, 2)
|
|
79
|
+
layout.addWidget(self.presets_edit, 2, 0, 1, 2)
|
|
80
|
+
layout.addWidget(self.interval_label, 3, 0)
|
|
81
|
+
layout.addWidget(self.interval_spinbox, 3, 1)
|
|
82
|
+
layout.addWidget(self.hide_checkbox, 4, 0, 1, 2)
|
|
83
|
+
layout.addWidget(self.hint_label, 5, 0, 1, 2)
|
|
84
|
+
layout.addWidget(self.button_box, 6, 0, 1, 2)
|
|
85
|
+
|
|
86
|
+
def settings(self) -> Dict[str, Any]:
|
|
87
|
+
"""對話框上目前的設定。"""
|
|
88
|
+
return {
|
|
89
|
+
"enabled": self.enable_checkbox.isChecked(),
|
|
90
|
+
"presets": normalize_playlist(self.presets_edit.toPlainText()),
|
|
91
|
+
"interval_minutes": self.interval_spinbox.value(),
|
|
92
|
+
"hide_window": self.hide_checkbox.isChecked(),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
def accept(self) -> None:
|
|
96
|
+
settings = self.settings()
|
|
97
|
+
user_setting_dict[SETTING_KEY] = settings
|
|
98
|
+
write_user_setting()
|
|
99
|
+
front_engine_logger.info(f"[SignageDialog] saved | {len(settings['presets'])} preset(s)")
|
|
100
|
+
super().accept()
|