frontengine 1.0.32__tar.gz → 1.0.34__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.32 → frontengine-1.0.34}/PKG-INFO +45 -1
- {frontengine-1.0.32 → frontengine-1.0.34}/README.md +44 -0
- frontengine-1.0.34/frontengine/show/focus_shield/focus_shield_widget.py +137 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/show/pet/desktop_pet.py +24 -94
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/show/text/draw_text.py +12 -1
- frontengine-1.0.34/frontengine/show/wallpaper/wallpaper_widget.py +155 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/main_ui.py +25 -4
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/menu/preset_menu.py +85 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/menu/settings_menu.py +61 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/page/control_center/control_center_ui.py +27 -2
- frontengine-1.0.34/frontengine/ui/page/focus/focus_setting_ui.py +211 -0
- frontengine-1.0.34/frontengine/ui/page/wallpaper/wallpaper_setting_ui.py +247 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/user_setting/user_setting_file.py +6 -0
- frontengine-1.0.34/frontengine/utils/autostart/autostart_service.py +172 -0
- frontengine-1.0.34/frontengine/utils/focus_shield/focus_shield.py +93 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/multi_language/english.py +39 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/multi_language/traditional_chinese.py +39 -0
- frontengine-1.0.34/frontengine/utils/platform_info/platform_info.py +303 -0
- frontengine-1.0.34/frontengine/utils/playlist/playlist.py +135 -0
- frontengine-1.0.34/frontengine/utils/plugins/__init__.py +0 -0
- frontengine-1.0.34/frontengine/utils/plugins/plugin_loader.py +138 -0
- frontengine-1.0.34/frontengine/utils/power_mode/__init__.py +0 -0
- frontengine-1.0.34/frontengine/utils/power_mode/power_mode.py +29 -0
- frontengine-1.0.34/frontengine/utils/preset_schedule/__init__.py +0 -0
- frontengine-1.0.34/frontengine/utils/redirect_manager/__init__.py +0 -0
- frontengine-1.0.34/frontengine/utils/smart_pause/__init__.py +0 -0
- frontengine-1.0.34/frontengine/utils/system_stats/__init__.py +0 -0
- frontengine-1.0.34/frontengine/utils/text_source/__init__.py +0 -0
- frontengine-1.0.34/frontengine/utils/theme_schedule/__init__.py +0 -0
- frontengine-1.0.34/frontengine/utils/weather/__init__.py +0 -0
- frontengine-1.0.34/frontengine/utils/workshop/__init__.py +0 -0
- frontengine-1.0.34/frontengine/utils/workshop/workshop_content.py +149 -0
- frontengine-1.0.34/frontengine/worker/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine.egg-info/PKG-INFO +45 -1
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine.egg-info/SOURCES.txt +22 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/pyproject.toml +1 -1
- {frontengine-1.0.32 → frontengine-1.0.34}/LICENSE +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/__main__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/show/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/show/base_widget.py +0 -0
- {frontengine-1.0.32/frontengine/show/gif → frontengine-1.0.34/frontengine/show/focus_shield}/__init__.py +0 -0
- {frontengine-1.0.32/frontengine/show/image → frontengine-1.0.34/frontengine/show/gif}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/show/gif/paint_gif.py +0 -0
- {frontengine-1.0.32/frontengine/show/load → frontengine-1.0.34/frontengine/show/image}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/show/image/paint_image.py +0 -0
- {frontengine-1.0.32/frontengine/show/particle → frontengine-1.0.34/frontengine/show/load}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/show/load/load_someone_make_ui.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/show/overlay_factory.py +0 -0
- {frontengine-1.0.32/frontengine/show/pet → frontengine-1.0.34/frontengine/show/particle}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/show/particle/particle_ui.py +0 -0
- {frontengine-1.0.32/frontengine/show/presentation → frontengine-1.0.34/frontengine/show/pet}/__init__.py +0 -0
- {frontengine-1.0.32/frontengine/show/scene → frontengine-1.0.34/frontengine/show/presentation}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/show/presentation/annotation_overlay.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/show/presentation/cursor_effects.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/show/presentation/keystroke_display.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/show/presentation/magnifier.py +0 -0
- {frontengine-1.0.32/frontengine/show/screen_care → frontengine-1.0.34/frontengine/show/scene}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/show/scene/extend_graphic_scene.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/show/scene/extend_graphic_view.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/show/scene/scene.py +0 -0
- {frontengine-1.0.32/frontengine/show/sound_player → frontengine-1.0.34/frontengine/show/screen_care}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/show/screen_care/screen_filter.py +0 -0
- {frontengine-1.0.32/frontengine/show/text → frontengine-1.0.34/frontengine/show/sound_player}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/show/sound_player/sound_effect.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/show/sound_player/sound_player.py +0 -0
- {frontengine-1.0.32/frontengine/show/video → frontengine-1.0.34/frontengine/show/text}/__init__.py +0 -0
- {frontengine-1.0.32/frontengine/show/web → frontengine-1.0.34/frontengine/show/video}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/show/video/video_player.py +0 -0
- {frontengine-1.0.32/frontengine/system_tray → frontengine-1.0.34/frontengine/show/wallpaper}/__init__.py +0 -0
- {frontengine-1.0.32/frontengine/ui → frontengine-1.0.34/frontengine/show/web}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/show/web/webview.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/show/window_helpers.py +0 -0
- {frontengine-1.0.32/frontengine/ui/color → frontengine-1.0.34/frontengine/system_tray}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/system_tray/extend_system_tray.py +0 -0
- {frontengine-1.0.32/frontengine/ui/dialog → frontengine-1.0.34/frontengine/ui}/__init__.py +0 -0
- {frontengine-1.0.32/frontengine/ui/menu → frontengine-1.0.34/frontengine/ui/color}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/color/global_color.py +0 -0
- {frontengine-1.0.32/frontengine/ui/page → frontengine-1.0.34/frontengine/ui/dialog}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/dialog/choose_file_dialog.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/dialog/hotkey_settings_dialog.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/dialog/save_file_dialog.py +0 -0
- {frontengine-1.0.32/frontengine/ui/page/control_center → frontengine-1.0.34/frontengine/ui/menu}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/menu/help_menu.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/menu/how_to_menu.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/menu/language_menu.py +0 -0
- {frontengine-1.0.32/frontengine/ui/page/gif → frontengine-1.0.34/frontengine/ui/page}/__init__.py +0 -0
- {frontengine-1.0.32/frontengine/ui/page/image → frontengine-1.0.34/frontengine/ui/page/control_center}/__init__.py +0 -0
- {frontengine-1.0.32/frontengine/ui/page/particle → frontengine-1.0.34/frontengine/ui/page/focus}/__init__.py +0 -0
- {frontengine-1.0.32/frontengine/ui/page/pet → frontengine-1.0.34/frontengine/ui/page/gif}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/page/gif/gif_setting_ui.py +0 -0
- {frontengine-1.0.32/frontengine/ui/page/presentation → frontengine-1.0.34/frontengine/ui/page/image}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/page/image/image_setting_ui.py +0 -0
- {frontengine-1.0.32/frontengine/ui/page/scene_setting → frontengine-1.0.34/frontengine/ui/page/particle}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/page/particle/particle_setting_ui.py +0 -0
- {frontengine-1.0.32/frontengine/ui/page/scene_setting/scene_page → frontengine-1.0.34/frontengine/ui/page/pet}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/page/pet/pet_setting_ui.py +0 -0
- {frontengine-1.0.32/frontengine/ui/page/screen_care → frontengine-1.0.34/frontengine/ui/page/presentation}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/page/presentation/presentation_setting_ui.py +0 -0
- {frontengine-1.0.32/frontengine/ui/page/sound_player → frontengine-1.0.34/frontengine/ui/page/scene_setting}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/page/scene_setting/scene_manager.py +0 -0
- {frontengine-1.0.32/frontengine/ui/page/text → frontengine-1.0.34/frontengine/ui/page/scene_setting/scene_page}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/page/scene_setting/scene_page/base_scene_page.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/page/scene_setting/scene_page/gif.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/page/scene_setting/scene_page/image.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/page/scene_setting/scene_page/registry.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/page/scene_setting/scene_page/sound.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/page/scene_setting/scene_page/text.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/page/scene_setting/scene_page/video.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/page/scene_setting/scene_page/web.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/page/scene_setting/scene_setting_ui.py +0 -0
- {frontengine-1.0.32/frontengine/ui/page/video → frontengine-1.0.34/frontengine/ui/page/screen_care}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/page/screen_care/screen_care_setting_ui.py +0 -0
- {frontengine-1.0.32/frontengine/ui/page/web → frontengine-1.0.34/frontengine/ui/page/sound_player}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/page/sound_player/sound_player_setting_ui.py +0 -0
- {frontengine-1.0.32/frontengine/user_setting → frontengine-1.0.34/frontengine/ui/page/text}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/page/text/text_setting_ui.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/page/utils.py +0 -0
- {frontengine-1.0.32/frontengine/utils → frontengine-1.0.34/frontengine/ui/page/video}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/page/video/video_setting_ui.py +0 -0
- {frontengine-1.0.32/frontengine/utils/audio_meter → frontengine-1.0.34/frontengine/ui/page/wallpaper}/__init__.py +0 -0
- {frontengine-1.0.32/frontengine/utils/break_reminder → frontengine-1.0.34/frontengine/ui/page/web}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/ui/page/web/web_setting_ui.py +0 -0
- {frontengine-1.0.32/frontengine/utils/browser → frontengine-1.0.34/frontengine/user_setting}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/user_setting/preset_repository.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/user_setting/scene_setting.py +0 -0
- {frontengine-1.0.32/frontengine/utils/critical_exit → frontengine-1.0.34/frontengine/utils}/__init__.py +0 -0
- {frontengine-1.0.32/frontengine/utils/file → frontengine-1.0.34/frontengine/utils/audio_meter}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/audio_meter/audio_envelope.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/audio_meter/screen_audio.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/audio_meter/system_audio_meter.py +0 -0
- {frontengine-1.0.32/frontengine/utils/focus_timer → frontengine-1.0.34/frontengine/utils/autostart}/__init__.py +0 -0
- {frontengine-1.0.32/frontengine/utils/hotkey → frontengine-1.0.34/frontengine/utils/break_reminder}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/break_reminder/break_reminder.py +0 -0
- {frontengine-1.0.32/frontengine/utils/input_watch → frontengine-1.0.34/frontengine/utils/browser}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/browser/browser.py +0 -0
- {frontengine-1.0.32/frontengine/utils/json → frontengine-1.0.34/frontengine/utils/critical_exit}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/critical_exit/check_key_is_press.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/critical_exit/critical_exit.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/critical_exit/win32_vk.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/exception/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/exception/exception_tags.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/exception/exceptions.py +0 -0
- {frontengine-1.0.32/frontengine/utils/logging → frontengine-1.0.34/frontengine/utils/file}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/file/open/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/file/open/open_file.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/file/save/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/file/save/save_file.py +0 -0
- {frontengine-1.0.32/frontengine/utils/multi_language → frontengine-1.0.34/frontengine/utils/focus_shield}/__init__.py +0 -0
- {frontengine-1.0.32/frontengine/utils/pet_chat → frontengine-1.0.34/frontengine/utils/focus_timer}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/focus_timer/focus_timer.py +0 -0
- {frontengine-1.0.32/frontengine/utils/preset_schedule → frontengine-1.0.34/frontengine/utils/hotkey}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/hotkey/hotkey_service.py +0 -0
- {frontengine-1.0.32/frontengine/utils/redirect_manager → frontengine-1.0.34/frontengine/utils/input_watch}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/input_watch/input_watch_service.py +0 -0
- {frontengine-1.0.32/frontengine/utils/smart_pause → frontengine-1.0.34/frontengine/utils/json}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/json/json_file.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/json/json_repository.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/json_format/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/json_format/json_process.py +0 -0
- {frontengine-1.0.32/frontengine/utils/system_stats → frontengine-1.0.34/frontengine/utils/logging}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/logging/loggin_instance.py +0 -0
- {frontengine-1.0.32/frontengine/utils/text_source → frontengine-1.0.34/frontengine/utils/multi_language}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/multi_language/france.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/multi_language/germany.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/multi_language/italy.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/multi_language/language_wrapper.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/multi_language/russian.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/multi_language/simplified_chinese.py +0 -0
- {frontengine-1.0.32/frontengine/utils/theme_schedule → frontengine-1.0.34/frontengine/utils/pet_chat}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/pet_chat/pet_chat_service.py +0 -0
- {frontengine-1.0.32/frontengine/utils/weather → frontengine-1.0.34/frontengine/utils/platform_info}/__init__.py +0 -0
- {frontengine-1.0.32/frontengine/worker → frontengine-1.0.34/frontengine/utils/playlist}/__init__.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/preset_schedule/preset_schedule_service.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/redirect_manager/redirect_manager_class.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/smart_pause/smart_pause_service.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/system_stats/system_stats.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/text_source/text_source.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/theme_schedule/theme_schedule_service.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/weather/weather_service.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/utils/web_url.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine/worker/qthread_worker.py +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine.egg-info/dependency_links.txt +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine.egg-info/entry_points.txt +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine.egg-info/requires.txt +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/frontengine.egg-info/top_level.txt +0 -0
- {frontengine-1.0.32 → frontengine-1.0.34}/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.34
|
|
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
|
|
@@ -85,6 +85,50 @@ Audio features are Windows-only and degrade to "no pulse" elsewhere.
|
|
|
85
85
|
|
|
86
86
|
---
|
|
87
87
|
|
|
88
|
+
## Focus
|
|
89
|
+
|
|
90
|
+
Two overlays for when the screen is competing with your work, both on the
|
|
91
|
+
**Focus** tab:
|
|
92
|
+
|
|
93
|
+
- **Dim background windows** — everything except the window you are working in
|
|
94
|
+
is shaded, at an adjustable strength. Off Windows, where the active window
|
|
95
|
+
cannot be identified without a native binding, it shades the whole screen.
|
|
96
|
+
- **Cover a distraction** — mask a strip of the screen: the taskbar, a
|
|
97
|
+
notification corner, an edge, or all of it.
|
|
98
|
+
|
|
99
|
+
Both pass clicks through, so what they cover still works — it just stops pulling
|
|
100
|
+
your eye.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Wallpaper
|
|
105
|
+
|
|
106
|
+
Play a folder of images and animations *beneath* every window, from the
|
|
107
|
+
**Wallpaper** tab:
|
|
108
|
+
|
|
109
|
+
- **A playlist per monitor** — each screen points at its own folder, changes on
|
|
110
|
+
its own timer, and can be shuffled or read recursively.
|
|
111
|
+
- **React to audio** — the wallpaper can pulse with your speakers' output level,
|
|
112
|
+
using the same meter-only reading as the pet (no audio is captured).
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Community content
|
|
117
|
+
|
|
118
|
+
- **Steam Workshop** — subscribed items are picked up from Steam's own
|
|
119
|
+
`steamapps/workshop/content` folder: presets are imported and pet packs are
|
|
120
|
+
listed with their paths, from **Presets → Import Workshop content**.
|
|
121
|
+
Publishing to the Workshop needs the Steamworks SDK and is not built in.
|
|
122
|
+
- **Plugins** — a `plugins/` folder can add its own tabs, either through a
|
|
123
|
+
`FRONTENGINE_TABS` mapping or a `register(registry)` hook.
|
|
124
|
+
|
|
125
|
+
> A plugin is Python and runs with the same privileges as FrontEngine — it
|
|
126
|
+
> cannot be sandboxed. Loading is **off by default** (Settings → Load plugins),
|
|
127
|
+
> every load is logged, and one broken plugin is skipped rather than stopping
|
|
128
|
+
> the app. Only install plugins you trust.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
88
132
|
## Install
|
|
89
133
|
|
|
90
134
|
- **System Requirements**
|
|
@@ -61,6 +61,50 @@ Audio features are Windows-only and degrade to "no pulse" elsewhere.
|
|
|
61
61
|
|
|
62
62
|
---
|
|
63
63
|
|
|
64
|
+
## Focus
|
|
65
|
+
|
|
66
|
+
Two overlays for when the screen is competing with your work, both on the
|
|
67
|
+
**Focus** tab:
|
|
68
|
+
|
|
69
|
+
- **Dim background windows** — everything except the window you are working in
|
|
70
|
+
is shaded, at an adjustable strength. Off Windows, where the active window
|
|
71
|
+
cannot be identified without a native binding, it shades the whole screen.
|
|
72
|
+
- **Cover a distraction** — mask a strip of the screen: the taskbar, a
|
|
73
|
+
notification corner, an edge, or all of it.
|
|
74
|
+
|
|
75
|
+
Both pass clicks through, so what they cover still works — it just stops pulling
|
|
76
|
+
your eye.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Wallpaper
|
|
81
|
+
|
|
82
|
+
Play a folder of images and animations *beneath* every window, from the
|
|
83
|
+
**Wallpaper** tab:
|
|
84
|
+
|
|
85
|
+
- **A playlist per monitor** — each screen points at its own folder, changes on
|
|
86
|
+
its own timer, and can be shuffled or read recursively.
|
|
87
|
+
- **React to audio** — the wallpaper can pulse with your speakers' output level,
|
|
88
|
+
using the same meter-only reading as the pet (no audio is captured).
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Community content
|
|
93
|
+
|
|
94
|
+
- **Steam Workshop** — subscribed items are picked up from Steam's own
|
|
95
|
+
`steamapps/workshop/content` folder: presets are imported and pet packs are
|
|
96
|
+
listed with their paths, from **Presets → Import Workshop content**.
|
|
97
|
+
Publishing to the Workshop needs the Steamworks SDK and is not built in.
|
|
98
|
+
- **Plugins** — a `plugins/` folder can add its own tabs, either through a
|
|
99
|
+
`FRONTENGINE_TABS` mapping or a `register(registry)` hook.
|
|
100
|
+
|
|
101
|
+
> A plugin is Python and runs with the same privileges as FrontEngine — it
|
|
102
|
+
> cannot be sandboxed. Loading is **off by default** (Settings → Load plugins),
|
|
103
|
+
> every load is logged, and one broken plugin is skipped rather than stopping
|
|
104
|
+
> the app. Only install plugins you trust.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
64
108
|
## Install
|
|
65
109
|
|
|
66
110
|
- **System Requirements**
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
專注用的兩種遮罩:
|
|
3
|
+
- DimBackgroundWidget:把作用中視窗以外的地方壓暗,視線自然留在手上的工作
|
|
4
|
+
- DistractionMaskWidget:蓋住指定的螢幕區域(工作列、通知角落)
|
|
5
|
+
兩者都是點擊穿透的,所以被遮住的地方仍然可以正常點擊。
|
|
6
|
+
|
|
7
|
+
Two focus masks: one dims everything except the active window so your eye stays
|
|
8
|
+
on the work, the other covers a chosen strip of the screen (the taskbar, a
|
|
9
|
+
notification corner). Both pass clicks through, so what is covered still works.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
from typing import List, Optional, Tuple
|
|
15
|
+
|
|
16
|
+
from PySide6.QtCore import QRect, QTimer
|
|
17
|
+
from PySide6.QtGui import QColor, QPainter
|
|
18
|
+
|
|
19
|
+
from frontengine.show.base_widget import BaseWidget
|
|
20
|
+
from frontengine.utils.focus_shield.focus_shield import (
|
|
21
|
+
REGION_BOTTOM, clamp_percent, mask_rect, surrounding_rects,
|
|
22
|
+
)
|
|
23
|
+
from frontengine.utils.logging.loggin_instance import front_engine_logger
|
|
24
|
+
|
|
25
|
+
DEFAULT_DIM = 45
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def active_window_rect() -> Optional[Tuple[int, int, int, int]]:
|
|
29
|
+
"""
|
|
30
|
+
回傳目前作用中視窗的 (x, y, w, h);取不到或非 Windows 回傳 None
|
|
31
|
+
(其他平台會退化成「整片壓暗」,仍然可用)。
|
|
32
|
+
The active window's rectangle, or None off Windows — where the dimmer simply
|
|
33
|
+
shades the whole screen instead.
|
|
34
|
+
"""
|
|
35
|
+
if sys.platform != "win32":
|
|
36
|
+
return None
|
|
37
|
+
try:
|
|
38
|
+
import ctypes
|
|
39
|
+
from ctypes import wintypes
|
|
40
|
+
|
|
41
|
+
user32 = ctypes.windll.user32
|
|
42
|
+
hwnd = user32.GetForegroundWindow()
|
|
43
|
+
if not hwnd:
|
|
44
|
+
return None
|
|
45
|
+
rect = wintypes.RECT()
|
|
46
|
+
if not user32.GetWindowRect(hwnd, ctypes.byref(rect)):
|
|
47
|
+
return None
|
|
48
|
+
return (rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top)
|
|
49
|
+
except Exception as error: # pragma: no cover - Win32 boundary
|
|
50
|
+
front_engine_logger.debug(f"[focus_shield] active window unavailable: {error!r}")
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class DimBackgroundWidget(BaseWidget):
|
|
55
|
+
"""
|
|
56
|
+
背景變暗:只有作用中視窗保持原樣,其餘壓暗;視窗換了會自動跟上。
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
REFRESH_MS = 250
|
|
60
|
+
|
|
61
|
+
def __init__(self, dim: int = DEFAULT_DIM) -> None:
|
|
62
|
+
front_engine_logger.info(f"[DimBackgroundWidget] Init | dim={dim}")
|
|
63
|
+
super().__init__()
|
|
64
|
+
self.opacity = 1.0
|
|
65
|
+
self.dim = clamp_percent(dim, DEFAULT_DIM)
|
|
66
|
+
self.shaded: List[Tuple[int, int, int, int]] = []
|
|
67
|
+
self._window_provider = active_window_rect
|
|
68
|
+
self._timer = QTimer(self)
|
|
69
|
+
self._timer.timeout.connect(self.refresh)
|
|
70
|
+
|
|
71
|
+
def set_window_provider(self, provider) -> None:
|
|
72
|
+
"""注入作用中視窗來源(測試用)。"""
|
|
73
|
+
self._window_provider = provider
|
|
74
|
+
|
|
75
|
+
def set_dim(self, dim: int) -> None:
|
|
76
|
+
self.dim = clamp_percent(dim, DEFAULT_DIM)
|
|
77
|
+
self.update()
|
|
78
|
+
|
|
79
|
+
def start_following(self, interval_ms: int = REFRESH_MS) -> None:
|
|
80
|
+
self.refresh()
|
|
81
|
+
self._timer.start(max(50, int(interval_ms)))
|
|
82
|
+
|
|
83
|
+
def screen_rect(self) -> Tuple[int, int, int, int]:
|
|
84
|
+
geometry = self.geometry()
|
|
85
|
+
return (geometry.x(), geometry.y(), geometry.width(), geometry.height())
|
|
86
|
+
|
|
87
|
+
def refresh(self) -> None:
|
|
88
|
+
"""重新計算要壓暗的區塊。"""
|
|
89
|
+
try:
|
|
90
|
+
active = self._window_provider()
|
|
91
|
+
except Exception: # pragma: no cover - defensive around providers
|
|
92
|
+
active = None
|
|
93
|
+
rects = surrounding_rects(self.screen_rect(), active)
|
|
94
|
+
origin_x, origin_y = self.x(), self.y()
|
|
95
|
+
# 換算成本視窗的座標系 / Convert to this widget's own coordinates
|
|
96
|
+
self.shaded = [(x - origin_x, y - origin_y, w, h) for x, y, w, h in rects]
|
|
97
|
+
self.update()
|
|
98
|
+
|
|
99
|
+
def draw_content(self, painter: QPainter) -> None:
|
|
100
|
+
shade = QColor(0, 0, 0, int(255 * self.dim / 100.0))
|
|
101
|
+
for x, y, width, height in self.shaded:
|
|
102
|
+
painter.fillRect(QRect(x, y, width, height), shade)
|
|
103
|
+
|
|
104
|
+
def closeEvent(self, event) -> None:
|
|
105
|
+
if self._timer.isActive():
|
|
106
|
+
self._timer.stop()
|
|
107
|
+
super().closeEvent(event)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class DistractionMaskWidget(BaseWidget):
|
|
111
|
+
"""
|
|
112
|
+
干擾遮罩:把螢幕的某個區域(預設底部,正好蓋住工作列)以不透明色蓋掉。
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
def __init__(self, region: str = REGION_BOTTOM, percent: int = 6,
|
|
116
|
+
color: str = "#101010") -> None:
|
|
117
|
+
front_engine_logger.info(
|
|
118
|
+
f"[DistractionMaskWidget] Init | region={region}, percent={percent}")
|
|
119
|
+
super().__init__()
|
|
120
|
+
self.opacity = 1.0
|
|
121
|
+
self.region = region
|
|
122
|
+
self.percent = clamp_percent(percent)
|
|
123
|
+
self.mask_color = QColor(color) if QColor(color).isValid() else QColor("#101010")
|
|
124
|
+
|
|
125
|
+
def geometry_for(self, screen_rect: Tuple[int, int, int, int]) -> QRect:
|
|
126
|
+
"""算出這個遮罩該放在哪裡、多大。"""
|
|
127
|
+
x, y, width, height = mask_rect(screen_rect, self.region, self.percent)
|
|
128
|
+
return QRect(x, y, width, height)
|
|
129
|
+
|
|
130
|
+
def set_region(self, region: Optional[str] = None, percent: Optional[int] = None) -> None:
|
|
131
|
+
if region is not None:
|
|
132
|
+
self.region = region
|
|
133
|
+
if percent is not None:
|
|
134
|
+
self.percent = clamp_percent(percent)
|
|
135
|
+
|
|
136
|
+
def draw_content(self, painter: QPainter) -> None:
|
|
137
|
+
painter.fillRect(self.rect(), self.mask_color)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import random as _random_module
|
|
3
|
-
import sys
|
|
4
3
|
from datetime import datetime, timedelta
|
|
5
4
|
from pathlib import Path
|
|
6
5
|
from urllib.parse import urlparse
|
|
@@ -16,6 +15,12 @@ from frontengine.show.window_helpers import apply_overlay_window_flags
|
|
|
16
15
|
from frontengine.user_setting.user_setting_file import user_setting_dict
|
|
17
16
|
from frontengine.utils.audio_meter.audio_envelope import AudioEnvelope
|
|
18
17
|
from frontengine.utils.logging.loggin_instance import front_engine_logger
|
|
18
|
+
from frontengine.utils.power_mode.power_mode import scaled_interval
|
|
19
|
+
from frontengine.utils.platform_info.platform_info import (
|
|
20
|
+
idle_seconds as platform_idle_seconds,
|
|
21
|
+
read_battery as platform_read_battery,
|
|
22
|
+
standable_windows,
|
|
23
|
+
)
|
|
19
24
|
from frontengine.utils.multi_language.language_wrapper import language_wrapper
|
|
20
25
|
|
|
21
26
|
# 行為模式 / Behaviour modes
|
|
@@ -212,75 +217,17 @@ def derive_visual_state(dragging: bool, airborne: bool, surface: str, motion_sta
|
|
|
212
217
|
return STATE_WALK
|
|
213
218
|
|
|
214
219
|
|
|
215
|
-
# 列舉可站立視窗時要略過的外殼視窗類別
|
|
216
|
-
# Shell window classes skipped when enumerating standable windows.
|
|
217
|
-
_PLATFORM_SKIP_CLASSES = {"WorkerW", "Progman", "Shell_TrayWnd", "Button"}
|
|
218
|
-
|
|
219
|
-
|
|
220
220
|
def windows_platforms(exclude_hwnds=()) -> list:
|
|
221
221
|
"""
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
Return the top edges of visible top-level windows as standable platforms
|
|
225
|
-
(left, right, top_y). Windows only; returns [] elsewhere or on error.
|
|
222
|
+
回傳目前可站立的視窗上緣 (left, right, top_y);各平台實作見 platform_info。
|
|
223
|
+
Standable window top edges; per-platform implementations live in platform_info.
|
|
226
224
|
"""
|
|
227
|
-
|
|
228
|
-
return []
|
|
229
|
-
try:
|
|
230
|
-
import ctypes
|
|
231
|
-
from ctypes import wintypes
|
|
232
|
-
|
|
233
|
-
user32 = ctypes.windll.user32
|
|
234
|
-
exclude = {int(h) for h in exclude_hwnds}
|
|
235
|
-
platforms: list = []
|
|
236
|
-
|
|
237
|
-
@ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
|
|
238
|
-
def _collect(hwnd, _lparam):
|
|
239
|
-
if int(hwnd) in exclude or not user32.IsWindowVisible(hwnd):
|
|
240
|
-
return True
|
|
241
|
-
rect = wintypes.RECT()
|
|
242
|
-
if not user32.GetWindowRect(hwnd, ctypes.byref(rect)):
|
|
243
|
-
return True
|
|
244
|
-
width = rect.right - rect.left
|
|
245
|
-
height = rect.bottom - rect.top
|
|
246
|
-
if width < 80 or height < 40:
|
|
247
|
-
return True
|
|
248
|
-
class_buffer = ctypes.create_unicode_buffer(64)
|
|
249
|
-
user32.GetClassNameW(hwnd, class_buffer, 64)
|
|
250
|
-
if class_buffer.value in _PLATFORM_SKIP_CLASSES:
|
|
251
|
-
return True
|
|
252
|
-
platforms.append((rect.left, rect.right, rect.top))
|
|
253
|
-
return True
|
|
254
|
-
|
|
255
|
-
user32.EnumWindows(_collect, 0)
|
|
256
|
-
return platforms
|
|
257
|
-
except Exception as error: # pragma: no cover - Win32 boundary
|
|
258
|
-
front_engine_logger.warning(f"[windows_platforms] error: {error!r}")
|
|
259
|
-
return []
|
|
225
|
+
return standable_windows(exclude_hwnds)
|
|
260
226
|
|
|
261
227
|
|
|
262
228
|
def idle_seconds():
|
|
263
|
-
"""
|
|
264
|
-
|
|
265
|
-
Seconds since the last user input, or None when unavailable (Windows only).
|
|
266
|
-
"""
|
|
267
|
-
if sys.platform != "win32":
|
|
268
|
-
return None
|
|
269
|
-
try:
|
|
270
|
-
import ctypes
|
|
271
|
-
|
|
272
|
-
class _LASTINPUTINFO(ctypes.Structure):
|
|
273
|
-
_fields_ = [("cbSize", ctypes.c_uint), ("dwTime", ctypes.c_uint)]
|
|
274
|
-
|
|
275
|
-
info = _LASTINPUTINFO()
|
|
276
|
-
info.cbSize = ctypes.sizeof(info)
|
|
277
|
-
if not ctypes.windll.user32.GetLastInputInfo(ctypes.byref(info)):
|
|
278
|
-
return None
|
|
279
|
-
elapsed_ms = ctypes.windll.kernel32.GetTickCount() - info.dwTime
|
|
280
|
-
return max(0.0, elapsed_ms / 1000.0)
|
|
281
|
-
except Exception as error: # pragma: no cover - Win32 boundary
|
|
282
|
-
front_engine_logger.warning(f"[idle_seconds] error: {error!r}")
|
|
283
|
-
return None
|
|
229
|
+
"""使用者未操作的秒數;取不到回傳 None(跨平台,見 platform_info)。"""
|
|
230
|
+
return platform_idle_seconds()
|
|
284
231
|
|
|
285
232
|
|
|
286
233
|
class PetTimeline:
|
|
@@ -335,35 +282,8 @@ def pop_due_reminders(now, reminders):
|
|
|
335
282
|
|
|
336
283
|
|
|
337
284
|
def read_battery():
|
|
338
|
-
"""
|
|
339
|
-
|
|
340
|
-
Return (percent, charging) or None when unavailable. Windows only.
|
|
341
|
-
"""
|
|
342
|
-
if sys.platform != "win32":
|
|
343
|
-
return None
|
|
344
|
-
try:
|
|
345
|
-
import ctypes
|
|
346
|
-
|
|
347
|
-
class _SPS(ctypes.Structure):
|
|
348
|
-
_fields_ = [
|
|
349
|
-
("ACLineStatus", ctypes.c_byte),
|
|
350
|
-
("BatteryFlag", ctypes.c_byte),
|
|
351
|
-
("BatteryLifePercent", ctypes.c_byte),
|
|
352
|
-
("SystemStatusFlag", ctypes.c_byte),
|
|
353
|
-
("BatteryLifeTime", ctypes.c_ulong),
|
|
354
|
-
("BatteryFullLifeTime", ctypes.c_ulong),
|
|
355
|
-
]
|
|
356
|
-
|
|
357
|
-
status = _SPS()
|
|
358
|
-
if not ctypes.windll.kernel32.GetSystemPowerStatus(ctypes.byref(status)):
|
|
359
|
-
return None
|
|
360
|
-
percent = status.BatteryLifePercent & 0xFF
|
|
361
|
-
if percent == 255: # unknown / no battery
|
|
362
|
-
return None
|
|
363
|
-
return (int(percent), status.ACLineStatus == 1)
|
|
364
|
-
except Exception as error: # pragma: no cover - Win32 boundary
|
|
365
|
-
front_engine_logger.warning(f"[read_battery] error: {error!r}")
|
|
366
|
-
return None
|
|
285
|
+
"""回傳 (電量百分比, 是否充電中);沒有電池或取不到回傳 None(跨平台)。"""
|
|
286
|
+
return platform_read_battery()
|
|
367
287
|
|
|
368
288
|
|
|
369
289
|
def nearest_peer(center, peers):
|
|
@@ -1147,6 +1067,9 @@ class DesktopPetWidget(BaseWidget):
|
|
|
1147
1067
|
|
|
1148
1068
|
self._timer = QTimer(self)
|
|
1149
1069
|
self._timer.timeout.connect(self._on_tick)
|
|
1070
|
+
# 省電模式與原始更新間隔 / Low-power state and the base tick interval
|
|
1071
|
+
self._low_power = False
|
|
1072
|
+
self._tick_interval_ms = 33
|
|
1150
1073
|
|
|
1151
1074
|
# Speech bubbles / chatter / mood
|
|
1152
1075
|
self._talk = bool(talk)
|
|
@@ -1417,6 +1340,12 @@ class DesktopPetWidget(BaseWidget):
|
|
|
1417
1340
|
painter.drawPixmap(
|
|
1418
1341
|
QRect((self.width() - width) // 2, self.height() - height, width, height), pixmap)
|
|
1419
1342
|
|
|
1343
|
+
def set_low_power(self, enabled: bool) -> None:
|
|
1344
|
+
"""省電模式:把移動更新拉慢(畫面較不順,但省電)。"""
|
|
1345
|
+
self._low_power = bool(enabled)
|
|
1346
|
+
if self._timer.isActive():
|
|
1347
|
+
self._timer.start(scaled_interval(self._tick_interval_ms, self._low_power))
|
|
1348
|
+
|
|
1420
1349
|
def start_moving(self, bounds: Tuple[int, int, int, int], interval_ms: int = 33) -> None:
|
|
1421
1350
|
front_engine_logger.info(f"[DesktopPetWidget] start_moving | bounds={bounds}")
|
|
1422
1351
|
left, top, right, bottom = bounds
|
|
@@ -1424,7 +1353,8 @@ class DesktopPetWidget(BaseWidget):
|
|
|
1424
1353
|
self.motion.x = float(left)
|
|
1425
1354
|
self.motion.y = float(bottom - self.pet_size)
|
|
1426
1355
|
self.move(int(self.motion.x), int(self.motion.y))
|
|
1427
|
-
self.
|
|
1356
|
+
self._tick_interval_ms = max(10, int(interval_ms))
|
|
1357
|
+
self._timer.start(scaled_interval(self._tick_interval_ms, self._low_power))
|
|
1428
1358
|
self._schedule_chatter()
|
|
1429
1359
|
self._reminder_timer.start(10000)
|
|
1430
1360
|
if self._timeline_start is None:
|
|
@@ -3,6 +3,7 @@ from PySide6.QtGui import QPainter, QFont, QColor, QFontMetrics
|
|
|
3
3
|
|
|
4
4
|
from frontengine.show.base_widget import BaseWidget
|
|
5
5
|
from frontengine.utils.logging.loggin_instance import front_engine_logger
|
|
6
|
+
from frontengine.utils.power_mode.power_mode import scaled_interval
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
_ALIGNMENT_MAP = {
|
|
@@ -35,6 +36,7 @@ class TextWidget(BaseWidget):
|
|
|
35
36
|
self.draw_font = QFont(self.font().family(), self.font_size)
|
|
36
37
|
|
|
37
38
|
# Marquee (horizontal scroll) state
|
|
39
|
+
self.low_power: bool = False
|
|
38
40
|
self.marquee: bool = False
|
|
39
41
|
self.marquee_speed: int = 4
|
|
40
42
|
self.marquee_offset: int = 0
|
|
@@ -83,7 +85,7 @@ class TextWidget(BaseWidget):
|
|
|
83
85
|
self.marquee_speed = 4
|
|
84
86
|
if self.marquee:
|
|
85
87
|
self.marquee_offset = self.width()
|
|
86
|
-
self._marquee_timer.start(30)
|
|
88
|
+
self._marquee_timer.start(scaled_interval(30, self.low_power))
|
|
87
89
|
else:
|
|
88
90
|
self._marquee_timer.stop()
|
|
89
91
|
|
|
@@ -116,6 +118,15 @@ class TextWidget(BaseWidget):
|
|
|
116
118
|
self.text = text
|
|
117
119
|
self.update()
|
|
118
120
|
|
|
121
|
+
def set_low_power(self, enabled: bool) -> None:
|
|
122
|
+
"""省電模式:拉慢文字來源與跑馬燈的更新,降低重繪次數。"""
|
|
123
|
+
self.low_power = bool(enabled)
|
|
124
|
+
if self._source_timer.isActive() and self.text_source is not None:
|
|
125
|
+
self._source_timer.start(scaled_interval(
|
|
126
|
+
getattr(self.text_source, "refresh_interval_ms", 1000), self.low_power))
|
|
127
|
+
if self._marquee_timer.isActive():
|
|
128
|
+
self._marquee_timer.start(scaled_interval(30, self.low_power))
|
|
129
|
+
|
|
119
130
|
def _tick_marquee(self) -> None:
|
|
120
131
|
text_width = QFontMetrics(self.draw_font).horizontalAdvance(self.text)
|
|
121
132
|
self.marquee_offset -= self.marquee_speed
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
桌布覆蓋層:在所有視窗底下播放圖片或動圖,可依系統音量脈動(音訊反應桌布),
|
|
3
|
+
並由播放清單定時換張。音量來源沿用寵物已經在用的系統峰值電表,只讀電表、
|
|
4
|
+
不擷取音訊內容。
|
|
5
|
+
|
|
6
|
+
A wallpaper layer: plays an image or animation beneath every window, optionally
|
|
7
|
+
pulsing with the system output level, with a playlist swapping it over time.
|
|
8
|
+
The level comes from the same meter the pet uses — a meter reading only, never
|
|
9
|
+
captured audio.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from PySide6.QtCore import QRect, QTimer
|
|
17
|
+
from PySide6.QtGui import QMovie, QPainter, QPixmap
|
|
18
|
+
|
|
19
|
+
from frontengine.show.base_widget import BaseWidget
|
|
20
|
+
from frontengine.utils.audio_meter.audio_envelope import AudioEnvelope
|
|
21
|
+
from frontengine.utils.logging.loggin_instance import front_engine_logger
|
|
22
|
+
|
|
23
|
+
_ANIMATED_SUFFIXES = (".gif", ".webp")
|
|
24
|
+
MIN_REACT_SCALE = 1.0
|
|
25
|
+
MAX_REACT_SCALE = 1.25
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def react_scale(level, strength: int = 100) -> float:
|
|
29
|
+
"""
|
|
30
|
+
把音量 (0~1) 換成桌布的縮放比例:安靜時 1.0,最大聲時最多放大 25%。
|
|
31
|
+
strength 是效果強度百分比,0 代表完全不反應。
|
|
32
|
+
Map an audio level to a wallpaper scale: 1.0 when quiet, up to +25% loud.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
clamped = max(0.0, min(1.0, float(level)))
|
|
36
|
+
except (TypeError, ValueError):
|
|
37
|
+
return MIN_REACT_SCALE
|
|
38
|
+
try:
|
|
39
|
+
share = max(0, min(100, int(strength))) / 100.0
|
|
40
|
+
except (TypeError, ValueError):
|
|
41
|
+
share = 1.0
|
|
42
|
+
return MIN_REACT_SCALE + (MAX_REACT_SCALE - MIN_REACT_SCALE) * clamped * share
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class WallpaperWidget(BaseWidget):
|
|
46
|
+
"""
|
|
47
|
+
桌布:顯示一張圖或一段動圖,位於所有視窗底下;可選擇隨系統音量脈動。
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
REACT_INTERVAL_MS = 33
|
|
51
|
+
|
|
52
|
+
def __init__(self, media_path: str = "", audio_react: bool = False,
|
|
53
|
+
react_strength: int = 100) -> None:
|
|
54
|
+
front_engine_logger.info(
|
|
55
|
+
f"[WallpaperWidget] Init | path={media_path}, audio_react={audio_react}")
|
|
56
|
+
super().__init__()
|
|
57
|
+
self.opacity = 1.0
|
|
58
|
+
self.media_path: Optional[Path] = None
|
|
59
|
+
self._movie: Optional[QMovie] = None
|
|
60
|
+
self._pixmap: Optional[QPixmap] = None
|
|
61
|
+
self.audio_react = bool(audio_react)
|
|
62
|
+
self.react_strength = max(0, min(100, int(react_strength)))
|
|
63
|
+
self.scale = MIN_REACT_SCALE
|
|
64
|
+
self._level_provider = None
|
|
65
|
+
self._envelope = AudioEnvelope()
|
|
66
|
+
self._react_timer = QTimer(self)
|
|
67
|
+
self._react_timer.timeout.connect(self._update_scale)
|
|
68
|
+
if media_path:
|
|
69
|
+
self.set_media(media_path)
|
|
70
|
+
|
|
71
|
+
def set_media(self, media_path: str) -> bool:
|
|
72
|
+
"""換一張桌布;載不起來時保留原本的並回傳 False。"""
|
|
73
|
+
path = Path(media_path)
|
|
74
|
+
if not path.is_file():
|
|
75
|
+
front_engine_logger.warning(f"[WallpaperWidget] missing media: {path}")
|
|
76
|
+
return False
|
|
77
|
+
if path.suffix.lower() in _ANIMATED_SUFFIXES:
|
|
78
|
+
movie = QMovie(str(path))
|
|
79
|
+
if not movie.isValid():
|
|
80
|
+
return False
|
|
81
|
+
self._stop_movie()
|
|
82
|
+
self._movie = movie
|
|
83
|
+
self._pixmap = None
|
|
84
|
+
movie.frameChanged.connect(self.update)
|
|
85
|
+
movie.start()
|
|
86
|
+
else:
|
|
87
|
+
pixmap = QPixmap(str(path))
|
|
88
|
+
if pixmap.isNull():
|
|
89
|
+
return False
|
|
90
|
+
self._stop_movie()
|
|
91
|
+
self._pixmap = pixmap
|
|
92
|
+
self._movie = None
|
|
93
|
+
self.media_path = path
|
|
94
|
+
self.update()
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
def set_audio_level_provider(self, provider) -> None:
|
|
98
|
+
"""設定音量來源(回傳 0~1 或 None)。"""
|
|
99
|
+
self._level_provider = provider
|
|
100
|
+
self._envelope.reset()
|
|
101
|
+
|
|
102
|
+
def set_audio_react(self, enabled: bool, strength: Optional[int] = None) -> None:
|
|
103
|
+
"""開關音訊反應並視需要調整強度。"""
|
|
104
|
+
self.audio_react = bool(enabled)
|
|
105
|
+
if strength is not None:
|
|
106
|
+
self.react_strength = max(0, min(100, int(strength)))
|
|
107
|
+
if self.audio_react:
|
|
108
|
+
self._react_timer.start(self.REACT_INTERVAL_MS)
|
|
109
|
+
else:
|
|
110
|
+
self._react_timer.stop()
|
|
111
|
+
self.scale = MIN_REACT_SCALE
|
|
112
|
+
self.update()
|
|
113
|
+
|
|
114
|
+
def _update_scale(self) -> None:
|
|
115
|
+
"""取一次音量、平滑後換成縮放比例。"""
|
|
116
|
+
if not self.audio_react or self._level_provider is None:
|
|
117
|
+
self.scale = MIN_REACT_SCALE
|
|
118
|
+
return
|
|
119
|
+
try:
|
|
120
|
+
level = self._level_provider()
|
|
121
|
+
except Exception: # pragma: no cover - defensive around providers
|
|
122
|
+
level = None
|
|
123
|
+
if level is None:
|
|
124
|
+
self.scale = MIN_REACT_SCALE
|
|
125
|
+
self._envelope.reset()
|
|
126
|
+
else:
|
|
127
|
+
self.scale = react_scale(self._envelope.push(level), self.react_strength)
|
|
128
|
+
self.update()
|
|
129
|
+
|
|
130
|
+
def _stop_movie(self) -> None:
|
|
131
|
+
if self._movie is not None:
|
|
132
|
+
self._movie.stop()
|
|
133
|
+
self._movie = None
|
|
134
|
+
|
|
135
|
+
def current_pixmap(self) -> Optional[QPixmap]:
|
|
136
|
+
if self._movie is not None:
|
|
137
|
+
return self._movie.currentPixmap()
|
|
138
|
+
return self._pixmap
|
|
139
|
+
|
|
140
|
+
def draw_content(self, painter: QPainter) -> None:
|
|
141
|
+
pixmap = self.current_pixmap()
|
|
142
|
+
if pixmap is None or pixmap.isNull():
|
|
143
|
+
return
|
|
144
|
+
width = int(self.width() * self.scale)
|
|
145
|
+
height = int(self.height() * self.scale)
|
|
146
|
+
# 放大時往外溢出,維持置中,畫面才不會出現黑邊
|
|
147
|
+
# Overflow outward and stay centred so no bars appear while pulsing.
|
|
148
|
+
painter.drawPixmap(
|
|
149
|
+
QRect((self.width() - width) // 2, (self.height() - height) // 2, width, height), pixmap)
|
|
150
|
+
|
|
151
|
+
def closeEvent(self, event) -> None:
|
|
152
|
+
if self._react_timer.isActive():
|
|
153
|
+
self._react_timer.stop()
|
|
154
|
+
self._stop_movie()
|
|
155
|
+
super().closeEvent(event)
|