frontengine 1.0.33__tar.gz → 1.0.35__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.
Files changed (198) hide show
  1. {frontengine-1.0.33 → frontengine-1.0.35}/PKG-INFO +45 -1
  2. {frontengine-1.0.33 → frontengine-1.0.35}/README.md +44 -0
  3. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/show/base_widget.py +25 -0
  4. frontengine-1.0.35/frontengine/show/focus_shield/focus_shield_widget.py +137 -0
  5. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/show/pet/desktop_pet.py +0 -1
  6. frontengine-1.0.35/frontengine/show/toast/toast_widget.py +120 -0
  7. frontengine-1.0.35/frontengine/show/wallpaper/wallpaper_widget.py +169 -0
  8. frontengine-1.0.35/frontengine/ui/dialog/app_profile_dialog.py +115 -0
  9. frontengine-1.0.35/frontengine/ui/dialog/reminder_dialog.py +139 -0
  10. frontengine-1.0.35/frontengine/ui/dialog/smart_pause_dialog.py +87 -0
  11. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/main_ui.py +94 -10
  12. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/menu/preset_menu.py +64 -0
  13. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/menu/settings_menu.py +61 -0
  14. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/page/control_center/control_center_ui.py +68 -3
  15. frontengine-1.0.35/frontengine/ui/page/focus/focus_setting_ui.py +211 -0
  16. frontengine-1.0.35/frontengine/ui/page/wallpaper/wallpaper_setting_ui.py +333 -0
  17. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/user_setting/user_setting_file.py +15 -0
  18. frontengine-1.0.35/frontengine/utils/app_profile/app_profile_service.py +102 -0
  19. frontengine-1.0.35/frontengine/utils/focus_shield/focus_shield.py +93 -0
  20. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/multi_language/english.py +64 -1
  21. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/multi_language/traditional_chinese.py +64 -2
  22. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/platform_info/platform_info.py +85 -0
  23. frontengine-1.0.35/frontengine/utils/playlist/playlist.py +135 -0
  24. frontengine-1.0.35/frontengine/utils/plugins/plugin_loader.py +138 -0
  25. frontengine-1.0.35/frontengine/utils/power_mode/__init__.py +0 -0
  26. frontengine-1.0.35/frontengine/utils/power_mode/power_mode.py +72 -0
  27. frontengine-1.0.35/frontengine/utils/preset_schedule/__init__.py +0 -0
  28. frontengine-1.0.35/frontengine/utils/redirect_manager/__init__.py +0 -0
  29. frontengine-1.0.35/frontengine/utils/reminder/__init__.py +0 -0
  30. frontengine-1.0.35/frontengine/utils/reminder/reminder_service.py +181 -0
  31. frontengine-1.0.35/frontengine/utils/smart_pause/__init__.py +0 -0
  32. frontengine-1.0.35/frontengine/utils/smart_pause/pause_rules.py +104 -0
  33. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/smart_pause/smart_pause_service.py +64 -9
  34. frontengine-1.0.35/frontengine/utils/system_stats/__init__.py +0 -0
  35. frontengine-1.0.35/frontengine/utils/text_source/__init__.py +0 -0
  36. frontengine-1.0.35/frontengine/utils/theme_schedule/__init__.py +0 -0
  37. frontengine-1.0.35/frontengine/utils/weather/__init__.py +0 -0
  38. frontengine-1.0.35/frontengine/utils/workshop/__init__.py +0 -0
  39. frontengine-1.0.35/frontengine/utils/workshop/workshop_content.py +149 -0
  40. frontengine-1.0.35/frontengine/worker/__init__.py +0 -0
  41. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine.egg-info/PKG-INFO +45 -1
  42. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine.egg-info/SOURCES.txt +26 -0
  43. {frontengine-1.0.33 → frontengine-1.0.35}/pyproject.toml +1 -1
  44. frontengine-1.0.33/frontengine/utils/power_mode/power_mode.py +0 -29
  45. {frontengine-1.0.33 → frontengine-1.0.35}/LICENSE +0 -0
  46. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/__init__.py +0 -0
  47. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/__main__.py +0 -0
  48. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/show/__init__.py +0 -0
  49. {frontengine-1.0.33/frontengine/show/gif → frontengine-1.0.35/frontengine/show/focus_shield}/__init__.py +0 -0
  50. {frontengine-1.0.33/frontengine/show/image → frontengine-1.0.35/frontengine/show/gif}/__init__.py +0 -0
  51. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/show/gif/paint_gif.py +0 -0
  52. {frontengine-1.0.33/frontengine/show/load → frontengine-1.0.35/frontengine/show/image}/__init__.py +0 -0
  53. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/show/image/paint_image.py +0 -0
  54. {frontengine-1.0.33/frontengine/show/particle → frontengine-1.0.35/frontengine/show/load}/__init__.py +0 -0
  55. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/show/load/load_someone_make_ui.py +0 -0
  56. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/show/overlay_factory.py +0 -0
  57. {frontengine-1.0.33/frontengine/show/pet → frontengine-1.0.35/frontengine/show/particle}/__init__.py +0 -0
  58. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/show/particle/particle_ui.py +0 -0
  59. {frontengine-1.0.33/frontengine/show/presentation → frontengine-1.0.35/frontengine/show/pet}/__init__.py +0 -0
  60. {frontengine-1.0.33/frontengine/show/scene → frontengine-1.0.35/frontengine/show/presentation}/__init__.py +0 -0
  61. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/show/presentation/annotation_overlay.py +0 -0
  62. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/show/presentation/cursor_effects.py +0 -0
  63. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/show/presentation/keystroke_display.py +0 -0
  64. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/show/presentation/magnifier.py +0 -0
  65. {frontengine-1.0.33/frontengine/show/screen_care → frontengine-1.0.35/frontengine/show/scene}/__init__.py +0 -0
  66. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/show/scene/extend_graphic_scene.py +0 -0
  67. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/show/scene/extend_graphic_view.py +0 -0
  68. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/show/scene/scene.py +0 -0
  69. {frontengine-1.0.33/frontengine/show/sound_player → frontengine-1.0.35/frontengine/show/screen_care}/__init__.py +0 -0
  70. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/show/screen_care/screen_filter.py +0 -0
  71. {frontengine-1.0.33/frontengine/show/text → frontengine-1.0.35/frontengine/show/sound_player}/__init__.py +0 -0
  72. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/show/sound_player/sound_effect.py +0 -0
  73. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/show/sound_player/sound_player.py +0 -0
  74. {frontengine-1.0.33/frontengine/show/video → frontengine-1.0.35/frontengine/show/text}/__init__.py +0 -0
  75. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/show/text/draw_text.py +0 -0
  76. {frontengine-1.0.33/frontengine/show/web → frontengine-1.0.35/frontengine/show/toast}/__init__.py +0 -0
  77. {frontengine-1.0.33/frontengine/system_tray → frontengine-1.0.35/frontengine/show/video}/__init__.py +0 -0
  78. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/show/video/video_player.py +0 -0
  79. {frontengine-1.0.33/frontengine/ui → frontengine-1.0.35/frontengine/show/wallpaper}/__init__.py +0 -0
  80. {frontengine-1.0.33/frontengine/ui/color → frontengine-1.0.35/frontengine/show/web}/__init__.py +0 -0
  81. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/show/web/webview.py +0 -0
  82. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/show/window_helpers.py +0 -0
  83. {frontengine-1.0.33/frontengine/ui/dialog → frontengine-1.0.35/frontengine/system_tray}/__init__.py +0 -0
  84. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/system_tray/extend_system_tray.py +0 -0
  85. {frontengine-1.0.33/frontengine/ui/menu → frontengine-1.0.35/frontengine/ui}/__init__.py +0 -0
  86. {frontengine-1.0.33/frontengine/ui/page → frontengine-1.0.35/frontengine/ui/color}/__init__.py +0 -0
  87. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/color/global_color.py +0 -0
  88. {frontengine-1.0.33/frontengine/ui/page/control_center → frontengine-1.0.35/frontengine/ui/dialog}/__init__.py +0 -0
  89. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/dialog/choose_file_dialog.py +0 -0
  90. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/dialog/hotkey_settings_dialog.py +0 -0
  91. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/dialog/save_file_dialog.py +0 -0
  92. {frontengine-1.0.33/frontengine/ui/page/gif → frontengine-1.0.35/frontengine/ui/menu}/__init__.py +0 -0
  93. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/menu/help_menu.py +0 -0
  94. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/menu/how_to_menu.py +0 -0
  95. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/menu/language_menu.py +0 -0
  96. {frontengine-1.0.33/frontengine/ui/page/image → frontengine-1.0.35/frontengine/ui/page}/__init__.py +0 -0
  97. {frontengine-1.0.33/frontengine/ui/page/particle → frontengine-1.0.35/frontengine/ui/page/control_center}/__init__.py +0 -0
  98. {frontengine-1.0.33/frontengine/ui/page/pet → frontengine-1.0.35/frontengine/ui/page/focus}/__init__.py +0 -0
  99. {frontengine-1.0.33/frontengine/ui/page/presentation → frontengine-1.0.35/frontengine/ui/page/gif}/__init__.py +0 -0
  100. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/page/gif/gif_setting_ui.py +0 -0
  101. {frontengine-1.0.33/frontengine/ui/page/scene_setting → frontengine-1.0.35/frontengine/ui/page/image}/__init__.py +0 -0
  102. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/page/image/image_setting_ui.py +0 -0
  103. {frontengine-1.0.33/frontengine/ui/page/scene_setting/scene_page → frontengine-1.0.35/frontengine/ui/page/particle}/__init__.py +0 -0
  104. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/page/particle/particle_setting_ui.py +0 -0
  105. {frontengine-1.0.33/frontengine/ui/page/screen_care → frontengine-1.0.35/frontengine/ui/page/pet}/__init__.py +0 -0
  106. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/page/pet/pet_setting_ui.py +0 -0
  107. {frontengine-1.0.33/frontengine/ui/page/sound_player → frontengine-1.0.35/frontengine/ui/page/presentation}/__init__.py +0 -0
  108. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/page/presentation/presentation_setting_ui.py +0 -0
  109. {frontengine-1.0.33/frontengine/ui/page/text → frontengine-1.0.35/frontengine/ui/page/scene_setting}/__init__.py +0 -0
  110. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/page/scene_setting/scene_manager.py +0 -0
  111. {frontengine-1.0.33/frontengine/ui/page/video → frontengine-1.0.35/frontengine/ui/page/scene_setting/scene_page}/__init__.py +0 -0
  112. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/page/scene_setting/scene_page/base_scene_page.py +0 -0
  113. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/page/scene_setting/scene_page/gif.py +0 -0
  114. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/page/scene_setting/scene_page/image.py +0 -0
  115. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/page/scene_setting/scene_page/registry.py +0 -0
  116. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/page/scene_setting/scene_page/sound.py +0 -0
  117. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/page/scene_setting/scene_page/text.py +0 -0
  118. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/page/scene_setting/scene_page/video.py +0 -0
  119. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/page/scene_setting/scene_page/web.py +0 -0
  120. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/page/scene_setting/scene_setting_ui.py +0 -0
  121. {frontengine-1.0.33/frontengine/ui/page/web → frontengine-1.0.35/frontengine/ui/page/screen_care}/__init__.py +0 -0
  122. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/page/screen_care/screen_care_setting_ui.py +0 -0
  123. {frontengine-1.0.33/frontengine/user_setting → frontengine-1.0.35/frontengine/ui/page/sound_player}/__init__.py +0 -0
  124. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/page/sound_player/sound_player_setting_ui.py +0 -0
  125. {frontengine-1.0.33/frontengine/utils → frontengine-1.0.35/frontengine/ui/page/text}/__init__.py +0 -0
  126. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/page/text/text_setting_ui.py +0 -0
  127. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/page/utils.py +0 -0
  128. {frontengine-1.0.33/frontengine/utils/audio_meter → frontengine-1.0.35/frontengine/ui/page/video}/__init__.py +0 -0
  129. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/page/video/video_setting_ui.py +0 -0
  130. {frontengine-1.0.33/frontengine/utils/autostart → frontengine-1.0.35/frontengine/ui/page/wallpaper}/__init__.py +0 -0
  131. {frontengine-1.0.33/frontengine/utils/break_reminder → frontengine-1.0.35/frontengine/ui/page/web}/__init__.py +0 -0
  132. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/ui/page/web/web_setting_ui.py +0 -0
  133. {frontengine-1.0.33/frontengine/utils/browser → frontengine-1.0.35/frontengine/user_setting}/__init__.py +0 -0
  134. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/user_setting/preset_repository.py +0 -0
  135. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/user_setting/scene_setting.py +0 -0
  136. {frontengine-1.0.33/frontengine/utils/critical_exit → frontengine-1.0.35/frontengine/utils}/__init__.py +0 -0
  137. {frontengine-1.0.33/frontengine/utils/file → frontengine-1.0.35/frontengine/utils/app_profile}/__init__.py +0 -0
  138. {frontengine-1.0.33/frontengine/utils/focus_timer → frontengine-1.0.35/frontengine/utils/audio_meter}/__init__.py +0 -0
  139. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/audio_meter/audio_envelope.py +0 -0
  140. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/audio_meter/screen_audio.py +0 -0
  141. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/audio_meter/system_audio_meter.py +0 -0
  142. {frontengine-1.0.33/frontengine/utils/hotkey → frontengine-1.0.35/frontengine/utils/autostart}/__init__.py +0 -0
  143. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/autostart/autostart_service.py +0 -0
  144. {frontengine-1.0.33/frontengine/utils/input_watch → frontengine-1.0.35/frontengine/utils/break_reminder}/__init__.py +0 -0
  145. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/break_reminder/break_reminder.py +0 -0
  146. {frontengine-1.0.33/frontengine/utils/json → frontengine-1.0.35/frontengine/utils/browser}/__init__.py +0 -0
  147. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/browser/browser.py +0 -0
  148. {frontengine-1.0.33/frontengine/utils/logging → frontengine-1.0.35/frontengine/utils/critical_exit}/__init__.py +0 -0
  149. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/critical_exit/check_key_is_press.py +0 -0
  150. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/critical_exit/critical_exit.py +0 -0
  151. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/critical_exit/win32_vk.py +0 -0
  152. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/exception/__init__.py +0 -0
  153. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/exception/exception_tags.py +0 -0
  154. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/exception/exceptions.py +0 -0
  155. {frontengine-1.0.33/frontengine/utils/multi_language → frontengine-1.0.35/frontengine/utils/file}/__init__.py +0 -0
  156. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/file/open/__init__.py +0 -0
  157. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/file/open/open_file.py +0 -0
  158. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/file/save/__init__.py +0 -0
  159. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/file/save/save_file.py +0 -0
  160. {frontengine-1.0.33/frontengine/utils/pet_chat → frontengine-1.0.35/frontengine/utils/focus_shield}/__init__.py +0 -0
  161. {frontengine-1.0.33/frontengine/utils/platform_info → frontengine-1.0.35/frontengine/utils/focus_timer}/__init__.py +0 -0
  162. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/focus_timer/focus_timer.py +0 -0
  163. {frontengine-1.0.33/frontengine/utils/power_mode → frontengine-1.0.35/frontengine/utils/hotkey}/__init__.py +0 -0
  164. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/hotkey/hotkey_service.py +0 -0
  165. {frontengine-1.0.33/frontengine/utils/preset_schedule → frontengine-1.0.35/frontengine/utils/input_watch}/__init__.py +0 -0
  166. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/input_watch/input_watch_service.py +0 -0
  167. {frontengine-1.0.33/frontengine/utils/redirect_manager → frontengine-1.0.35/frontengine/utils/json}/__init__.py +0 -0
  168. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/json/json_file.py +0 -0
  169. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/json/json_repository.py +0 -0
  170. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/json_format/__init__.py +0 -0
  171. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/json_format/json_process.py +0 -0
  172. {frontengine-1.0.33/frontengine/utils/smart_pause → frontengine-1.0.35/frontengine/utils/logging}/__init__.py +0 -0
  173. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/logging/loggin_instance.py +0 -0
  174. {frontengine-1.0.33/frontengine/utils/system_stats → frontengine-1.0.35/frontengine/utils/multi_language}/__init__.py +0 -0
  175. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/multi_language/france.py +0 -0
  176. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/multi_language/germany.py +0 -0
  177. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/multi_language/italy.py +0 -0
  178. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/multi_language/language_wrapper.py +0 -0
  179. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/multi_language/russian.py +0 -0
  180. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/multi_language/simplified_chinese.py +0 -0
  181. {frontengine-1.0.33/frontengine/utils/text_source → frontengine-1.0.35/frontengine/utils/pet_chat}/__init__.py +0 -0
  182. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/pet_chat/pet_chat_service.py +0 -0
  183. {frontengine-1.0.33/frontengine/utils/theme_schedule → frontengine-1.0.35/frontengine/utils/platform_info}/__init__.py +0 -0
  184. {frontengine-1.0.33/frontengine/utils/weather → frontengine-1.0.35/frontengine/utils/playlist}/__init__.py +0 -0
  185. {frontengine-1.0.33/frontengine/worker → frontengine-1.0.35/frontengine/utils/plugins}/__init__.py +0 -0
  186. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/preset_schedule/preset_schedule_service.py +0 -0
  187. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/redirect_manager/redirect_manager_class.py +0 -0
  188. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/system_stats/system_stats.py +0 -0
  189. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/text_source/text_source.py +0 -0
  190. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/theme_schedule/theme_schedule_service.py +0 -0
  191. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/weather/weather_service.py +0 -0
  192. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/utils/web_url.py +0 -0
  193. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine/worker/qthread_worker.py +0 -0
  194. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine.egg-info/dependency_links.txt +0 -0
  195. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine.egg-info/entry_points.txt +0 -0
  196. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine.egg-info/requires.txt +0 -0
  197. {frontengine-1.0.33 → frontengine-1.0.35}/frontengine.egg-info/top_level.txt +0 -0
  198. {frontengine-1.0.33 → frontengine-1.0.35}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frontengine
3
- Version: 1.0.33
3
+ Version: 1.0.35
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**
@@ -8,6 +8,7 @@ from PySide6.QtWidgets import QWidget
8
8
  from frontengine.show.window_helpers import apply_overlay_window_flags, load_overlay_icon
9
9
  from frontengine.user_setting.user_setting_file import get_overlay_geometry, save_overlay_geometry
10
10
  from frontengine.utils.logging.loggin_instance import front_engine_logger
11
+ from frontengine.utils.power_mode.power_mode import DEFAULT_TIER, normalize_tier, tier_render_scale
11
12
 
12
13
 
13
14
  class BaseWidget(QWidget):
@@ -29,6 +30,9 @@ class BaseWidget(QWidget):
29
30
  # 綠幕背景色:設了就以不透明色填滿背景,方便 OBS 去背
30
31
  # Chroma key colour: fills the background opaquely so OBS can key it out.
31
32
  self.background_color: Optional[QColor] = None
33
+ # 畫質檔位:影響更新頻率上限與算圖縮放(見 utils/power_mode)
34
+ # Quality tier: caps the refresh rate and scales rendering (see utils/power_mode).
35
+ self.quality_tier: str = DEFAULT_TIER
32
36
  self._drag_origin = None
33
37
  self._geometry_restored = False
34
38
 
@@ -44,6 +48,27 @@ class BaseWidget(QWidget):
44
48
  front_engine_logger.info(f"{self.__class__.__name__} set_ui_variable | opacity: {opacity}")
45
49
  self.opacity = opacity
46
50
 
51
+ def set_quality_tier(self, tier: str) -> None:
52
+ """
53
+ 設定畫質檔位。基底類別只記下來並通知子類別,實際怎麼省由子類別決定
54
+ (例如放慢自己的計時器、用較低解析度算圖)。
55
+ Set the quality tier. The base class records it and tells the subclass;
56
+ what to actually give up - timer rate, render resolution - is the
57
+ subclass's call.
58
+ """
59
+ self.quality_tier = normalize_tier(tier)
60
+ front_engine_logger.info(
61
+ f"{self.__class__.__name__} set_quality_tier | tier: {self.quality_tier}")
62
+ self.apply_quality_tier()
63
+ self.update()
64
+
65
+ def apply_quality_tier(self) -> None:
66
+ """子類別覆寫這裡,把新的檔位套到自己的計時器/算圖上。"""
67
+
68
+ def render_scale(self) -> float:
69
+ """目前檔位的算圖縮放,1.0 表示原生解析度。"""
70
+ return tier_render_scale(self.quality_tier)
71
+
47
72
  def set_background_color(self, color) -> None:
48
73
  """
49
74
  設定綠幕背景色(傳 None 取消)。背景以不透明色填滿,內容再依原本的
@@ -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
@@ -0,0 +1,120 @@
1
+ """
2
+ 提示卡:在畫面上方短暫顯示一行字(提醒到期、規則觸發等),時間到自己關掉。
3
+ 點擊穿透,不會擋到底下的操作。
4
+
5
+ A toast: a line of text shown briefly near the top of the screen - a reminder
6
+ coming due, a rule firing - that closes itself. Click-through, so it never gets
7
+ in the way of what is underneath.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from typing import Optional
12
+
13
+ from PySide6.QtCore import Qt, QTimer
14
+ from PySide6.QtGui import QColor, QFont, QFontMetrics, QGuiApplication, QPainter
15
+
16
+ from frontengine.show.base_widget import BaseWidget
17
+ from frontengine.utils.logging.loggin_instance import front_engine_logger
18
+
19
+ DEFAULT_DURATION_MS = 5000
20
+ MIN_DURATION_MS = 500
21
+ _PADDING = 18
22
+ _CORNER_RADIUS = 10
23
+
24
+
25
+ def clamp_duration(value, fallback: int = DEFAULT_DURATION_MS) -> int:
26
+ """顯示時間至少半秒,避免設成 0 讓提示閃一下就不見。"""
27
+ try:
28
+ return max(MIN_DURATION_MS, int(value))
29
+ except (TypeError, ValueError):
30
+ return fallback
31
+
32
+
33
+ class ToastWidget(BaseWidget):
34
+ """
35
+ 一張自動消失的提示卡。文字寬度決定卡片寬度,位置預設貼在主螢幕上方置中。
36
+ An auto-dismissing toast. The card is sized to its text and sits centred
37
+ near the top of the primary screen by default.
38
+ """
39
+
40
+ def __init__(self, message: str = "", duration_ms: int = DEFAULT_DURATION_MS,
41
+ font_size: int = 18) -> None:
42
+ front_engine_logger.info(f"[ToastWidget] Init | message={message!r}")
43
+ super().__init__()
44
+ self.message: str = str(message)
45
+ self.duration_ms: int = clamp_duration(duration_ms)
46
+ self.text_color = QColor("#ffffff")
47
+ self.card_color = QColor(20, 20, 20, 230)
48
+ self.opacity = 1.0
49
+ # 提示卡不該被拖曳擺位,也不接受點擊
50
+ # A toast is never dragged into place and never takes clicks.
51
+ self.overlay_lockable = False
52
+ self._font = QFont()
53
+ self._font.setPointSize(max(8, int(font_size)))
54
+ self._close_timer = QTimer(self)
55
+ self._close_timer.setSingleShot(True)
56
+ self._close_timer.timeout.connect(self.close)
57
+ self.set_ui_window_flag(show_on_bottom=False)
58
+ self.resize_to_message()
59
+
60
+ def set_message(self, message: str) -> None:
61
+ """換一則訊息並重新計算卡片大小。"""
62
+ self.message = str(message)
63
+ self.resize_to_message()
64
+ self.update()
65
+
66
+ def card_size(self) -> tuple:
67
+ """依文字算出卡片的寬高(含內距)。"""
68
+ metrics = QFontMetrics(self._font)
69
+ width = metrics.horizontalAdvance(self.message) + _PADDING * 2
70
+ height = metrics.height() + _PADDING
71
+ return (max(80, width), max(36, height))
72
+
73
+ def resize_to_message(self) -> None:
74
+ width, height = self.card_size()
75
+ self.resize(width, height)
76
+ self.move_to_top_centre()
77
+
78
+ def move_to_top_centre(self, screen=None) -> None:
79
+ """擺到螢幕上方置中(離頂端一小段距離,避開全螢幕程式的標題列)。"""
80
+ target = screen or QGuiApplication.primaryScreen()
81
+ if target is None: # pragma: no cover - no screen at all
82
+ return
83
+ available = target.availableGeometry()
84
+ self.move(
85
+ available.x() + (available.width() - self.width()) // 2,
86
+ available.y() + max(24, available.height() // 12),
87
+ )
88
+
89
+ def show_toast(self, screen=None) -> None:
90
+ """顯示並在時間到時自動關閉。"""
91
+ self.resize_to_message()
92
+ self.move_to_top_centre(screen)
93
+ self.show()
94
+ self._close_timer.start(self.duration_ms)
95
+
96
+ def draw_content(self, painter: QPainter) -> None:
97
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
98
+ painter.setPen(Qt.PenStyle.NoPen)
99
+ painter.setBrush(self.card_color)
100
+ painter.drawRoundedRect(self.rect(), _CORNER_RADIUS, _CORNER_RADIUS)
101
+ painter.setFont(self._font)
102
+ painter.setPen(self.text_color)
103
+ painter.drawText(self.rect(), Qt.AlignmentFlag.AlignCenter, self.message)
104
+
105
+
106
+ def show_toast(message: str, duration_ms: int = DEFAULT_DURATION_MS,
107
+ screen=None) -> Optional[ToastWidget]:
108
+ """
109
+ 顯示一則提示卡並回傳它(訊息為空就不顯示,回傳 None)。呼叫端不必自己
110
+ 管生命週期:時間到會自己 close,而 BaseWidget 帶著 WA_DeleteOnClose。
111
+ Show a toast and hand it back (None for an empty message). Callers do not
112
+ have to manage its life: it closes itself, and BaseWidget carries
113
+ WA_DeleteOnClose.
114
+ """
115
+ text = str(message or "").strip()
116
+ if not text:
117
+ return None
118
+ toast = ToastWidget(text, duration_ms)
119
+ toast.show_toast(screen)
120
+ return toast
@@ -0,0 +1,169 @@
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, Qt, 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
+ from frontengine.utils.power_mode.power_mode import tier_interval
23
+
24
+ _ANIMATED_SUFFIXES = (".gif", ".webp")
25
+ MIN_REACT_SCALE = 1.0
26
+ MAX_REACT_SCALE = 1.25
27
+
28
+
29
+ def react_scale(level, strength: int = 100) -> float:
30
+ """
31
+ 把音量 (0~1) 換成桌布的縮放比例:安靜時 1.0,最大聲時最多放大 25%。
32
+ strength 是效果強度百分比,0 代表完全不反應。
33
+ Map an audio level to a wallpaper scale: 1.0 when quiet, up to +25% loud.
34
+ """
35
+ try:
36
+ clamped = max(0.0, min(1.0, float(level)))
37
+ except (TypeError, ValueError):
38
+ return MIN_REACT_SCALE
39
+ try:
40
+ share = max(0, min(100, int(strength))) / 100.0
41
+ except (TypeError, ValueError):
42
+ share = 1.0
43
+ return MIN_REACT_SCALE + (MAX_REACT_SCALE - MIN_REACT_SCALE) * clamped * share
44
+
45
+
46
+ class WallpaperWidget(BaseWidget):
47
+ """
48
+ 桌布:顯示一張圖或一段動圖,位於所有視窗底下;可選擇隨系統音量脈動。
49
+ """
50
+
51
+ REACT_INTERVAL_MS = 33
52
+
53
+ def __init__(self, media_path: str = "", audio_react: bool = False,
54
+ react_strength: int = 100) -> None:
55
+ front_engine_logger.info(
56
+ f"[WallpaperWidget] Init | path={media_path}, audio_react={audio_react}")
57
+ super().__init__()
58
+ self.opacity = 1.0
59
+ self.media_path: Optional[Path] = None
60
+ self._movie: Optional[QMovie] = None
61
+ self._pixmap: Optional[QPixmap] = None
62
+ self.audio_react = bool(audio_react)
63
+ self.react_strength = max(0, min(100, int(react_strength)))
64
+ self.scale = MIN_REACT_SCALE
65
+ self._level_provider = None
66
+ self._envelope = AudioEnvelope()
67
+ self._react_timer = QTimer(self)
68
+ self._react_timer.timeout.connect(self._update_scale)
69
+ if media_path:
70
+ self.set_media(media_path)
71
+
72
+ def set_media(self, media_path: str) -> bool:
73
+ """換一張桌布;載不起來時保留原本的並回傳 False。"""
74
+ path = Path(media_path)
75
+ if not path.is_file():
76
+ front_engine_logger.warning(f"[WallpaperWidget] missing media: {path}")
77
+ return False
78
+ if path.suffix.lower() in _ANIMATED_SUFFIXES:
79
+ movie = QMovie(str(path))
80
+ if not movie.isValid():
81
+ return False
82
+ self._stop_movie()
83
+ self._movie = movie
84
+ self._pixmap = None
85
+ movie.frameChanged.connect(self.update)
86
+ movie.start()
87
+ else:
88
+ pixmap = QPixmap(str(path))
89
+ if pixmap.isNull():
90
+ return False
91
+ self._stop_movie()
92
+ self._pixmap = pixmap
93
+ self._movie = None
94
+ self.media_path = path
95
+ self.update()
96
+ return True
97
+
98
+ def set_audio_level_provider(self, provider) -> None:
99
+ """設定音量來源(回傳 0~1 或 None)。"""
100
+ self._level_provider = provider
101
+ self._envelope.reset()
102
+
103
+ def set_audio_react(self, enabled: bool, strength: Optional[int] = None) -> None:
104
+ """開關音訊反應並視需要調整強度。"""
105
+ self.audio_react = bool(enabled)
106
+ if strength is not None:
107
+ self.react_strength = max(0, min(100, int(strength)))
108
+ if self.audio_react:
109
+ self._react_timer.start(tier_interval(self.REACT_INTERVAL_MS, self.quality_tier))
110
+ else:
111
+ self._react_timer.stop()
112
+ self.scale = MIN_REACT_SCALE
113
+ self.update()
114
+
115
+ def apply_quality_tier(self) -> None:
116
+ """畫質檔位改變時,跟著調整脈動的更新頻率(算圖縮放在繪製時套用)。"""
117
+ if self._react_timer.isActive():
118
+ self._react_timer.start(tier_interval(self.REACT_INTERVAL_MS, self.quality_tier))
119
+
120
+ def _update_scale(self) -> None:
121
+ """取一次音量、平滑後換成縮放比例。"""
122
+ if not self.audio_react or self._level_provider is None:
123
+ self.scale = MIN_REACT_SCALE
124
+ return
125
+ try:
126
+ level = self._level_provider()
127
+ except Exception: # pragma: no cover - defensive around providers
128
+ level = None
129
+ if level is None:
130
+ self.scale = MIN_REACT_SCALE
131
+ self._envelope.reset()
132
+ else:
133
+ self.scale = react_scale(self._envelope.push(level), self.react_strength)
134
+ self.update()
135
+
136
+ def _stop_movie(self) -> None:
137
+ if self._movie is not None:
138
+ self._movie.stop()
139
+ self._movie = None
140
+
141
+ def current_pixmap(self) -> Optional[QPixmap]:
142
+ if self._movie is not None:
143
+ return self._movie.currentPixmap()
144
+ return self._pixmap
145
+
146
+ def draw_content(self, painter: QPainter) -> None:
147
+ pixmap = self.current_pixmap()
148
+ if pixmap is None or pixmap.isNull():
149
+ return
150
+ # 低畫質檔位先把來源縮小再放大回來:算的像素少,看起來只是稍軟。
151
+ # Lower tiers shrink the source before scaling it back up: fewer pixels
152
+ # to push, at the cost of a slightly softer picture.
153
+ scale = self.render_scale()
154
+ if scale < 1.0:
155
+ pixmap = pixmap.scaled(
156
+ max(1, int(pixmap.width() * scale)), max(1, int(pixmap.height() * scale)),
157
+ Qt.AspectRatioMode.IgnoreAspectRatio, Qt.TransformationMode.FastTransformation)
158
+ width = int(self.width() * self.scale)
159
+ height = int(self.height() * self.scale)
160
+ # 放大時往外溢出,維持置中,畫面才不會出現黑邊
161
+ # Overflow outward and stay centred so no bars appear while pulsing.
162
+ painter.drawPixmap(
163
+ QRect((self.width() - width) // 2, (self.height() - height) // 2, width, height), pixmap)
164
+
165
+ def closeEvent(self, event) -> None:
166
+ if self._react_timer.isActive():
167
+ self._react_timer.stop()
168
+ self._stop_movie()
169
+ super().closeEvent(event)