frontengine 1.0.47__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.
Files changed (270) hide show
  1. {frontengine-1.0.47 → frontengine-1.0.48}/PKG-INFO +16 -1
  2. {frontengine-1.0.47 → frontengine-1.0.48}/README.md +15 -0
  3. frontengine-1.0.48/frontengine/show/canvas/whiteboard_widget.py +243 -0
  4. frontengine-1.0.48/frontengine/ui/dialog/signage_dialog.py +100 -0
  5. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/main_ui.py +22 -0
  6. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/menu/settings_menu.py +18 -0
  7. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/pet/pet_setting_ui.py +25 -4
  8. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/presentation/presentation_setting_ui.py +49 -3
  9. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/user_setting/user_setting_file.py +3 -0
  10. frontengine-1.0.48/frontengine/utils/audio_meter/microphone_meter.py +147 -0
  11. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/multi_language/english.py +12 -0
  12. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/multi_language/traditional_chinese.py +12 -0
  13. frontengine-1.0.48/frontengine/utils/signage/signage_service.py +156 -0
  14. frontengine-1.0.48/frontengine/utils/workshop/__init__.py +0 -0
  15. frontengine-1.0.48/frontengine/worker/__init__.py +0 -0
  16. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine.egg-info/PKG-INFO +16 -1
  17. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine.egg-info/SOURCES.txt +6 -0
  18. {frontengine-1.0.47 → frontengine-1.0.48}/pyproject.toml +1 -1
  19. {frontengine-1.0.47 → frontengine-1.0.48}/LICENSE +0 -0
  20. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/__init__.py +0 -0
  21. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/__main__.py +0 -0
  22. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/__init__.py +0 -0
  23. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/base_widget.py +0 -0
  24. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/camera/__init__.py +0 -0
  25. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/camera/camera_widget.py +0 -0
  26. {frontengine-1.0.47/frontengine/show/capture → frontengine-1.0.48/frontengine/show/canvas}/__init__.py +0 -0
  27. {frontengine-1.0.47/frontengine/show/focus_shield → frontengine-1.0.48/frontengine/show/capture}/__init__.py +0 -0
  28. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/capture/region_capture.py +0 -0
  29. {frontengine-1.0.47/frontengine/show/gaming → frontengine-1.0.48/frontengine/show/focus_shield}/__init__.py +0 -0
  30. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/focus_shield/focus_shield_widget.py +0 -0
  31. {frontengine-1.0.47/frontengine/show/gif → frontengine-1.0.48/frontengine/show/gaming}/__init__.py +0 -0
  32. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/gaming/crosshair_widget.py +0 -0
  33. {frontengine-1.0.47/frontengine/show/image → frontengine-1.0.48/frontengine/show/gif}/__init__.py +0 -0
  34. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/gif/paint_gif.py +0 -0
  35. {frontengine-1.0.47/frontengine/show/load → frontengine-1.0.48/frontengine/show/image}/__init__.py +0 -0
  36. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/image/paint_image.py +0 -0
  37. {frontengine-1.0.47/frontengine/show/measure → frontengine-1.0.48/frontengine/show/load}/__init__.py +0 -0
  38. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/load/load_someone_make_ui.py +0 -0
  39. {frontengine-1.0.47/frontengine/show/monitor → frontengine-1.0.48/frontengine/show/measure}/__init__.py +0 -0
  40. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/measure/measure_widget.py +0 -0
  41. {frontengine-1.0.47/frontengine/show/notes → frontengine-1.0.48/frontengine/show/monitor}/__init__.py +0 -0
  42. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/monitor/monitor_widget.py +0 -0
  43. {frontengine-1.0.47/frontengine/show/particle → frontengine-1.0.48/frontengine/show/notes}/__init__.py +0 -0
  44. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/notes/sticky_note_widget.py +0 -0
  45. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/overlay_factory.py +0 -0
  46. {frontengine-1.0.47/frontengine/show/pet → frontengine-1.0.48/frontengine/show/particle}/__init__.py +0 -0
  47. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/particle/particle_ui.py +0 -0
  48. {frontengine-1.0.47/frontengine/show/presentation → frontengine-1.0.48/frontengine/show/pet}/__init__.py +0 -0
  49. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/pet/desktop_pet.py +0 -0
  50. {frontengine-1.0.47/frontengine/show/scene → frontengine-1.0.48/frontengine/show/presentation}/__init__.py +0 -0
  51. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/presentation/annotation_overlay.py +0 -0
  52. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/presentation/cursor_effects.py +0 -0
  53. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/presentation/keystroke_display.py +0 -0
  54. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/presentation/magnifier.py +0 -0
  55. {frontengine-1.0.47/frontengine/show/screen_care → frontengine-1.0.48/frontengine/show/scene}/__init__.py +0 -0
  56. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/scene/extend_graphic_scene.py +0 -0
  57. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/scene/extend_graphic_view.py +0 -0
  58. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/scene/scene.py +0 -0
  59. {frontengine-1.0.47/frontengine/show/sound_player → frontengine-1.0.48/frontengine/show/screen_care}/__init__.py +0 -0
  60. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/screen_care/color_vision_widget.py +0 -0
  61. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/screen_care/screen_filter.py +0 -0
  62. {frontengine-1.0.47/frontengine/show/spectrum → frontengine-1.0.48/frontengine/show/sound_player}/__init__.py +0 -0
  63. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/sound_player/sound_effect.py +0 -0
  64. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/sound_player/sound_player.py +0 -0
  65. {frontengine-1.0.47/frontengine/show/teleprompter → frontengine-1.0.48/frontengine/show/spectrum}/__init__.py +0 -0
  66. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/spectrum/spectrum_widget.py +0 -0
  67. {frontengine-1.0.47/frontengine/show/text → frontengine-1.0.48/frontengine/show/teleprompter}/__init__.py +0 -0
  68. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/teleprompter/teleprompter_widget.py +0 -0
  69. {frontengine-1.0.47/frontengine/show/toast → frontengine-1.0.48/frontengine/show/text}/__init__.py +0 -0
  70. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/text/draw_text.py +0 -0
  71. {frontengine-1.0.47/frontengine/show/video → frontengine-1.0.48/frontengine/show/toast}/__init__.py +0 -0
  72. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/toast/toast_widget.py +0 -0
  73. {frontengine-1.0.47/frontengine/show/wallpaper → frontengine-1.0.48/frontengine/show/video}/__init__.py +0 -0
  74. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/video/video_player.py +0 -0
  75. {frontengine-1.0.47/frontengine/show/web → frontengine-1.0.48/frontengine/show/wallpaper}/__init__.py +0 -0
  76. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/wallpaper/wallpaper_widget.py +0 -0
  77. {frontengine-1.0.47/frontengine/system_tray → frontengine-1.0.48/frontengine/show/web}/__init__.py +0 -0
  78. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/web/webview.py +0 -0
  79. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/show/window_helpers.py +0 -0
  80. {frontengine-1.0.47/frontengine/ui → frontengine-1.0.48/frontengine/system_tray}/__init__.py +0 -0
  81. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/system_tray/extend_system_tray.py +0 -0
  82. {frontengine-1.0.47/frontengine/ui/color → frontengine-1.0.48/frontengine/ui}/__init__.py +0 -0
  83. {frontengine-1.0.47/frontengine/ui/dialog → frontengine-1.0.48/frontengine/ui/color}/__init__.py +0 -0
  84. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/color/global_color.py +0 -0
  85. {frontengine-1.0.47/frontengine/ui/menu → frontengine-1.0.48/frontengine/ui/dialog}/__init__.py +0 -0
  86. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/dialog/app_profile_dialog.py +0 -0
  87. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/dialog/choose_file_dialog.py +0 -0
  88. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/dialog/clipboard_dialog.py +0 -0
  89. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/dialog/hotkey_settings_dialog.py +0 -0
  90. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/dialog/reminder_dialog.py +0 -0
  91. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/dialog/remote_control_dialog.py +0 -0
  92. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/dialog/save_file_dialog.py +0 -0
  93. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/dialog/screen_privacy_dialog.py +0 -0
  94. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/dialog/screen_text_dialog.py +0 -0
  95. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/dialog/smart_pause_dialog.py +0 -0
  96. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/dialog/usage_report_dialog.py +0 -0
  97. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/dialog/window_pin_dialog.py +0 -0
  98. {frontengine-1.0.47/frontengine/ui/page → frontengine-1.0.48/frontengine/ui/menu}/__init__.py +0 -0
  99. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/menu/help_menu.py +0 -0
  100. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/menu/how_to_menu.py +0 -0
  101. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/menu/language_menu.py +0 -0
  102. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/menu/preset_menu.py +0 -0
  103. {frontengine-1.0.47/frontengine/ui/page/control_center → frontengine-1.0.48/frontengine/ui/page}/__init__.py +0 -0
  104. {frontengine-1.0.47/frontengine/ui/page/focus → frontengine-1.0.48/frontengine/ui/page/control_center}/__init__.py +0 -0
  105. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/control_center/control_center_ui.py +0 -0
  106. {frontengine-1.0.47/frontengine/ui/page/gif → frontengine-1.0.48/frontengine/ui/page/focus}/__init__.py +0 -0
  107. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/focus/focus_setting_ui.py +0 -0
  108. {frontengine-1.0.47/frontengine/ui/page/image → frontengine-1.0.48/frontengine/ui/page/gif}/__init__.py +0 -0
  109. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/gif/gif_setting_ui.py +0 -0
  110. {frontengine-1.0.47/frontengine/ui/page/particle → frontengine-1.0.48/frontengine/ui/page/image}/__init__.py +0 -0
  111. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/image/image_setting_ui.py +0 -0
  112. {frontengine-1.0.47/frontengine/ui/page/pet → frontengine-1.0.48/frontengine/ui/page/particle}/__init__.py +0 -0
  113. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/particle/particle_setting_ui.py +0 -0
  114. {frontengine-1.0.47/frontengine/ui/page/presentation → frontengine-1.0.48/frontengine/ui/page/pet}/__init__.py +0 -0
  115. {frontengine-1.0.47/frontengine/ui/page/scene_setting → frontengine-1.0.48/frontengine/ui/page/presentation}/__init__.py +0 -0
  116. {frontengine-1.0.47/frontengine/ui/page/scene_setting/scene_page → frontengine-1.0.48/frontengine/ui/page/scene_setting}/__init__.py +0 -0
  117. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_manager.py +0 -0
  118. {frontengine-1.0.47/frontengine/ui/page/screen_care → frontengine-1.0.48/frontengine/ui/page/scene_setting/scene_page}/__init__.py +0 -0
  119. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_page/base_scene_page.py +0 -0
  120. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_page/gif.py +0 -0
  121. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_page/image.py +0 -0
  122. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_page/registry.py +0 -0
  123. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_page/sound.py +0 -0
  124. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_page/text.py +0 -0
  125. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_page/video.py +0 -0
  126. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_page/web.py +0 -0
  127. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/scene_setting/scene_setting_ui.py +0 -0
  128. {frontengine-1.0.47/frontengine/ui/page/sound_player → frontengine-1.0.48/frontengine/ui/page/screen_care}/__init__.py +0 -0
  129. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/screen_care/screen_care_setting_ui.py +0 -0
  130. {frontengine-1.0.47/frontengine/ui/page/stream → frontengine-1.0.48/frontengine/ui/page/sound_player}/__init__.py +0 -0
  131. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/sound_player/sound_player_setting_ui.py +0 -0
  132. {frontengine-1.0.47/frontengine/ui/page/text → frontengine-1.0.48/frontengine/ui/page/stream}/__init__.py +0 -0
  133. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/stream/stream_setting_ui.py +0 -0
  134. {frontengine-1.0.47/frontengine/ui/page/tools → frontengine-1.0.48/frontengine/ui/page/text}/__init__.py +0 -0
  135. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/text/text_setting_ui.py +0 -0
  136. {frontengine-1.0.47/frontengine/ui/page/video → frontengine-1.0.48/frontengine/ui/page/tools}/__init__.py +0 -0
  137. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/tools/tools_setting_ui.py +0 -0
  138. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/utils.py +0 -0
  139. {frontengine-1.0.47/frontengine/ui/page/wallpaper → frontengine-1.0.48/frontengine/ui/page/video}/__init__.py +0 -0
  140. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/video/video_setting_ui.py +0 -0
  141. {frontengine-1.0.47/frontengine/ui/page/web → frontengine-1.0.48/frontengine/ui/page/wallpaper}/__init__.py +0 -0
  142. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/wallpaper/wallpaper_setting_ui.py +0 -0
  143. {frontengine-1.0.47/frontengine/ui/page/widgets → frontengine-1.0.48/frontengine/ui/page/web}/__init__.py +0 -0
  144. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/web/web_setting_ui.py +0 -0
  145. {frontengine-1.0.47/frontengine/user_setting → frontengine-1.0.48/frontengine/ui/page/widgets}/__init__.py +0 -0
  146. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/ui/page/widgets/widgets_setting_ui.py +0 -0
  147. {frontengine-1.0.47/frontengine/utils → frontengine-1.0.48/frontengine/user_setting}/__init__.py +0 -0
  148. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/user_setting/preset_repository.py +0 -0
  149. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/user_setting/scene_setting.py +0 -0
  150. {frontengine-1.0.47/frontengine/utils/app_profile → frontengine-1.0.48/frontengine/utils}/__init__.py +0 -0
  151. {frontengine-1.0.47/frontengine/utils/audio_meter → frontengine-1.0.48/frontengine/utils/app_profile}/__init__.py +0 -0
  152. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/app_profile/app_profile_service.py +0 -0
  153. {frontengine-1.0.47/frontengine/utils/autostart → frontengine-1.0.48/frontengine/utils/audio_meter}/__init__.py +0 -0
  154. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/audio_meter/audio_envelope.py +0 -0
  155. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/audio_meter/loopback_capture.py +0 -0
  156. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/audio_meter/screen_audio.py +0 -0
  157. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/audio_meter/spectrum_analyzer.py +0 -0
  158. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/audio_meter/system_audio_meter.py +0 -0
  159. {frontengine-1.0.47/frontengine/utils/break_reminder → frontengine-1.0.48/frontengine/utils/autostart}/__init__.py +0 -0
  160. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/autostart/autostart_service.py +0 -0
  161. {frontengine-1.0.47/frontengine/utils/browser → frontengine-1.0.48/frontengine/utils/break_reminder}/__init__.py +0 -0
  162. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/break_reminder/break_reminder.py +0 -0
  163. {frontengine-1.0.47/frontengine/utils/clipboard → frontengine-1.0.48/frontengine/utils/browser}/__init__.py +0 -0
  164. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/browser/browser.py +0 -0
  165. {frontengine-1.0.47/frontengine/utils/color_vision → frontengine-1.0.48/frontengine/utils/clipboard}/__init__.py +0 -0
  166. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/clipboard/clipboard_history.py +0 -0
  167. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/clipboard/clipboard_watcher.py +0 -0
  168. {frontengine-1.0.47/frontengine/utils/critical_exit → frontengine-1.0.48/frontengine/utils/color_vision}/__init__.py +0 -0
  169. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/color_vision/color_vision.py +0 -0
  170. {frontengine-1.0.47/frontengine/utils/file → frontengine-1.0.48/frontengine/utils/critical_exit}/__init__.py +0 -0
  171. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/critical_exit/check_key_is_press.py +0 -0
  172. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/critical_exit/critical_exit.py +0 -0
  173. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/critical_exit/win32_vk.py +0 -0
  174. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/exception/__init__.py +0 -0
  175. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/exception/exception_tags.py +0 -0
  176. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/exception/exceptions.py +0 -0
  177. {frontengine-1.0.47/frontengine/utils/focus_shield → frontengine-1.0.48/frontengine/utils/file}/__init__.py +0 -0
  178. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/file/open/__init__.py +0 -0
  179. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/file/open/open_file.py +0 -0
  180. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/file/save/__init__.py +0 -0
  181. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/file/save/save_file.py +0 -0
  182. {frontengine-1.0.47/frontengine/utils/focus_timer → frontengine-1.0.48/frontengine/utils/focus_shield}/__init__.py +0 -0
  183. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/focus_shield/focus_shield.py +0 -0
  184. {frontengine-1.0.47/frontengine/utils/hotkey → frontengine-1.0.48/frontengine/utils/focus_timer}/__init__.py +0 -0
  185. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/focus_timer/focus_timer.py +0 -0
  186. {frontengine-1.0.47/frontengine/utils/input_watch → frontengine-1.0.48/frontengine/utils/hotkey}/__init__.py +0 -0
  187. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/hotkey/hotkey_service.py +0 -0
  188. {frontengine-1.0.47/frontengine/utils/json → frontengine-1.0.48/frontengine/utils/input_watch}/__init__.py +0 -0
  189. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/input_watch/input_watch_service.py +0 -0
  190. {frontengine-1.0.47/frontengine/utils/logging → frontengine-1.0.48/frontengine/utils/json}/__init__.py +0 -0
  191. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/json/json_file.py +0 -0
  192. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/json/json_repository.py +0 -0
  193. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/json_format/__init__.py +0 -0
  194. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/json_format/json_process.py +0 -0
  195. {frontengine-1.0.47/frontengine/utils/measure → frontengine-1.0.48/frontengine/utils/logging}/__init__.py +0 -0
  196. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/logging/loggin_instance.py +0 -0
  197. {frontengine-1.0.47/frontengine/utils/midi → frontengine-1.0.48/frontengine/utils/measure}/__init__.py +0 -0
  198. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/measure/measure.py +0 -0
  199. {frontengine-1.0.47/frontengine/utils/multi_language → frontengine-1.0.48/frontengine/utils/midi}/__init__.py +0 -0
  200. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/midi/midi_input.py +0 -0
  201. {frontengine-1.0.47/frontengine/utils/now_playing → frontengine-1.0.48/frontengine/utils/multi_language}/__init__.py +0 -0
  202. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/multi_language/france.py +0 -0
  203. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/multi_language/germany.py +0 -0
  204. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/multi_language/italy.py +0 -0
  205. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/multi_language/language_wrapper.py +0 -0
  206. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/multi_language/russian.py +0 -0
  207. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/multi_language/simplified_chinese.py +0 -0
  208. {frontengine-1.0.47/frontengine/utils/obs → frontengine-1.0.48/frontengine/utils/now_playing}/__init__.py +0 -0
  209. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/now_playing/now_playing.py +0 -0
  210. {frontengine-1.0.47/frontengine/utils/pet_chat → frontengine-1.0.48/frontengine/utils/obs}/__init__.py +0 -0
  211. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/obs/obs_client.py +0 -0
  212. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/obs/obs_protocol.py +0 -0
  213. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/obs/websocket_frames.py +0 -0
  214. {frontengine-1.0.47/frontengine/utils/platform_info → frontengine-1.0.48/frontengine/utils/pet_chat}/__init__.py +0 -0
  215. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/pet_chat/pet_chat_service.py +0 -0
  216. {frontengine-1.0.47/frontengine/utils/playlist → frontengine-1.0.48/frontengine/utils/platform_info}/__init__.py +0 -0
  217. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/platform_info/platform_info.py +0 -0
  218. {frontengine-1.0.47/frontengine/utils/plugins → frontengine-1.0.48/frontengine/utils/playlist}/__init__.py +0 -0
  219. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/playlist/playlist.py +0 -0
  220. {frontengine-1.0.47/frontengine/utils/power_mode → frontengine-1.0.48/frontengine/utils/plugins}/__init__.py +0 -0
  221. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/plugins/plugin_loader.py +0 -0
  222. {frontengine-1.0.47/frontengine/utils/preset_schedule → frontengine-1.0.48/frontengine/utils/power_mode}/__init__.py +0 -0
  223. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/power_mode/power_mode.py +0 -0
  224. {frontengine-1.0.47/frontengine/utils/recording → frontengine-1.0.48/frontengine/utils/preset_schedule}/__init__.py +0 -0
  225. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/preset_schedule/preset_schedule_service.py +0 -0
  226. {frontengine-1.0.47/frontengine/utils/redirect_manager → frontengine-1.0.48/frontengine/utils/recording}/__init__.py +0 -0
  227. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/recording/frame_recorder.py +0 -0
  228. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/recording/gif_writer.py +0 -0
  229. {frontengine-1.0.47/frontengine/utils/reminder → frontengine-1.0.48/frontengine/utils/redirect_manager}/__init__.py +0 -0
  230. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/redirect_manager/redirect_manager_class.py +0 -0
  231. {frontengine-1.0.47/frontengine/utils/remote → frontengine-1.0.48/frontengine/utils/reminder}/__init__.py +0 -0
  232. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/reminder/reminder_service.py +0 -0
  233. {frontengine-1.0.47/frontengine/utils/screen_privacy → frontengine-1.0.48/frontengine/utils/remote}/__init__.py +0 -0
  234. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/remote/remote_server.py +0 -0
  235. {frontengine-1.0.47/frontengine/utils/screen_text → frontengine-1.0.48/frontengine/utils/screen_privacy}/__init__.py +0 -0
  236. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/screen_privacy/capture_affinity.py +0 -0
  237. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/screen_privacy/share_watch.py +0 -0
  238. {frontengine-1.0.47/frontengine/utils/smart_pause → frontengine-1.0.48/frontengine/utils/screen_text}/__init__.py +0 -0
  239. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/screen_text/screen_text_service.py +0 -0
  240. {frontengine-1.0.47/frontengine/utils/soundboard → frontengine-1.0.48/frontengine/utils/signage}/__init__.py +0 -0
  241. {frontengine-1.0.47/frontengine/utils/system_stats → frontengine-1.0.48/frontengine/utils/smart_pause}/__init__.py +0 -0
  242. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/smart_pause/pause_rules.py +0 -0
  243. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/smart_pause/smart_pause_service.py +0 -0
  244. {frontengine-1.0.47/frontengine/utils/text_source → frontengine-1.0.48/frontengine/utils/soundboard}/__init__.py +0 -0
  245. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/soundboard/soundboard.py +0 -0
  246. {frontengine-1.0.47/frontengine/utils/theme_schedule → frontengine-1.0.48/frontengine/utils/system_stats}/__init__.py +0 -0
  247. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/system_stats/system_stats.py +0 -0
  248. {frontengine-1.0.47/frontengine/utils/usage_tracking → frontengine-1.0.48/frontengine/utils/text_source}/__init__.py +0 -0
  249. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/text_source/text_source.py +0 -0
  250. {frontengine-1.0.47/frontengine/utils/virtual_camera → frontengine-1.0.48/frontengine/utils/theme_schedule}/__init__.py +0 -0
  251. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/theme_schedule/theme_schedule_service.py +0 -0
  252. {frontengine-1.0.47/frontengine/utils/weather → frontengine-1.0.48/frontengine/utils/usage_tracking}/__init__.py +0 -0
  253. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/usage_tracking/usage_service.py +0 -0
  254. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/usage_tracking/usage_tracker.py +0 -0
  255. {frontengine-1.0.47/frontengine/utils/window_pin → frontengine-1.0.48/frontengine/utils/virtual_camera}/__init__.py +0 -0
  256. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/virtual_camera/camera_feed.py +0 -0
  257. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/virtual_camera/virtual_camera.py +0 -0
  258. {frontengine-1.0.47/frontengine/utils/workshop → frontengine-1.0.48/frontengine/utils/weather}/__init__.py +0 -0
  259. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/weather/weather_service.py +0 -0
  260. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/web_url.py +0 -0
  261. {frontengine-1.0.47/frontengine/worker → frontengine-1.0.48/frontengine/utils/window_pin}/__init__.py +0 -0
  262. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/window_pin/window_layout.py +0 -0
  263. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/window_pin/window_pin.py +0 -0
  264. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/utils/workshop/workshop_content.py +0 -0
  265. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine/worker/qthread_worker.py +0 -0
  266. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine.egg-info/dependency_links.txt +0 -0
  267. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine.egg-info/entry_points.txt +0 -0
  268. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine.egg-info/requires.txt +0 -0
  269. {frontengine-1.0.47 → frontengine-1.0.48}/frontengine.egg-info/top_level.txt +0 -0
  270. {frontengine-1.0.47 → 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.47
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
@@ -204,6 +204,21 @@ Four things on the **Stream** tab, for playing and for going live:
204
204
 
205
205
  ---
206
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
+
207
222
  ## Remote control
208
223
 
209
224
  From **Settings → Remote control**:
@@ -180,6 +180,21 @@ Four things on the **Stream** tab, for playing and for going live:
180
180
 
181
181
  ---
182
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
+
183
198
  ## Remote control
184
199
 
185
200
  From **Settings → Remote control**:
@@ -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,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()
@@ -53,6 +53,8 @@ from frontengine.ui.dialog.remote_control_dialog import (
53
53
  MIDI_KEY, current_bindings, remote_enabled,
54
54
  )
55
55
  from frontengine.ui.dialog.screen_privacy_dialog import current_settings as current_privacy_settings
56
+ from frontengine.ui.dialog.signage_dialog import current_settings as current_signage_settings
57
+ from frontengine.utils.signage.signage_service import SignageService
56
58
  from frontengine.utils.midi.midi_input import MidiInput, binding_key
57
59
  from frontengine.utils.remote.remote_server import RemoteServer
58
60
  from frontengine.ui.dialog.smart_pause_dialog import current_rules
@@ -235,6 +237,13 @@ class FrontEngineMainUI(QMainWindow):
235
237
  if user_setting_dict.get(MIDI_KEY):
236
238
  self.midi_input.start(user_setting_dict.get("midi_device") or 0)
237
239
 
240
+ # 看板模式:依序輪播預設集,主視窗可以收起來
241
+ # Signage mode: rotate presets, with the main window out of the way.
242
+ self.signage_service = SignageService(config_provider=current_signage_settings)
243
+ self.signage_service.preset_due.connect(lambda name: apply_named_preset(self, name))
244
+ if current_signage_settings().get("enabled"):
245
+ self.start_signage()
246
+
238
247
  # 分享畫面時把覆蓋層藏出擷取結果(自己仍看得到)
239
248
  # Hide the overlays from the capture while sharing; they stay on screen here.
240
249
  self.share_watch_service = ShareWatchService(config_provider=current_privacy_settings)
@@ -508,6 +517,18 @@ class FrontEngineMainUI(QMainWindow):
508
517
  if action:
509
518
  self._handle_hotkey(action)
510
519
 
520
+ def start_signage(self) -> Optional[str]:
521
+ """開始輪播;設定要求的話也把主視窗收起來。"""
522
+ first = self.signage_service.start()
523
+ if first is not None and current_signage_settings().get("hide_window", True):
524
+ self.hide()
525
+ return first
526
+
527
+ def stop_signage(self) -> None:
528
+ """停止輪播並把主視窗放回來。"""
529
+ self.signage_service.stop()
530
+ self.show()
531
+
511
532
  def _on_sharing_changed(self, sharing: bool, match: str) -> None:
512
533
  """
513
534
  偵測到會議程式開著/關掉時,自動把覆蓋層藏出擷取結果或放回去。
@@ -573,6 +594,7 @@ class FrontEngineMainUI(QMainWindow):
573
594
  # The background services to stop on close, outermost first.
574
595
  _CLOSING_SERVICES = (
575
596
  "preset_schedule_service", "theme_schedule_service", "usage_service",
597
+ "signage_service",
576
598
  "remote_server", "midi_input", "share_watch_service", "reminder_service",
577
599
  "app_profile_service", "smart_pause_service", "hotkey_service",
578
600
  )
@@ -12,6 +12,7 @@ from frontengine.ui.dialog.hotkey_settings_dialog import HotkeySettingsDialog
12
12
  from frontengine.ui.dialog.reminder_dialog import ReminderDialog
13
13
  from frontengine.ui.dialog.remote_control_dialog import RemoteControlDialog
14
14
  from frontengine.ui.dialog.screen_privacy_dialog import ScreenPrivacyDialog
15
+ from frontengine.ui.dialog.signage_dialog import SignageDialog
15
16
  from frontengine.ui.dialog.smart_pause_dialog import SmartPauseDialog
16
17
  from frontengine.ui.dialog.usage_report_dialog import UsageReportDialog
17
18
  from frontengine.user_setting.user_setting_file import (
@@ -89,6 +90,11 @@ def build_settings_menu(ui: "FrontEngineMainUI") -> None:
89
90
  menu.addAction(reminder_action)
90
91
  ui.reminder_action = reminder_action
91
92
 
93
+ signage_action = QAction(_t("settings_menu_signage", "Signage mode..."), menu)
94
+ signage_action.triggered.connect(lambda: _open_signage_dialog(ui))
95
+ menu.addAction(signage_action)
96
+ ui.signage_action = signage_action
97
+
92
98
  remote_action = QAction(_t("settings_menu_remote", "Remote control..."), menu)
93
99
  remote_action.triggered.connect(lambda: _open_remote_dialog(ui))
94
100
  menu.addAction(remote_action)
@@ -127,6 +133,18 @@ def build_settings_menu(ui: "FrontEngineMainUI") -> None:
127
133
  menu.addAction(import_action)
128
134
 
129
135
 
136
+ def _open_signage_dialog(ui: "FrontEngineMainUI") -> None:
137
+ """開看板設定;按下確定後立刻套用新的輪播狀態。"""
138
+ front_engine_logger.info("[SettingsMenu] open SignageDialog")
139
+ dialog = SignageDialog(ui)
140
+ if dialog.exec() != QDialog.DialogCode.Accepted:
141
+ return
142
+ if dialog.settings().get("enabled"):
143
+ ui.start_signage()
144
+ else:
145
+ ui.stop_signage()
146
+
147
+
130
148
  def _open_remote_dialog(ui: "FrontEngineMainUI") -> None:
131
149
  """開遙控設定,並把它接上 MIDI 的「學習」流程。"""
132
150
  front_engine_logger.info("[SettingsMenu] open RemoteControlDialog")
@@ -21,6 +21,7 @@ from frontengine.ui.page.utils import (
21
21
  resolve_preferred_monitor,
22
22
  )
23
23
  from frontengine.user_setting.user_setting_file import add_recent_file
24
+ from frontengine.utils.audio_meter.microphone_meter import microphone_level
24
25
  from frontengine.utils.audio_meter.screen_audio import audio_level_provider_for_screen
25
26
  from frontengine.utils.pet_chat.pet_chat_service import PetChatService
26
27
  from frontengine.utils.focus_timer.focus_timer import (
@@ -142,6 +143,12 @@ class PetSettingUI(QWidget):
142
143
  for label, value in (("0%", 0), ("25%", 25), ("50%", 50), ("75%", 75), ("100%", 100)):
143
144
  self.volume_combobox.addItem(label, value)
144
145
  self.volume_combobox.setCurrentText("100%")
146
+ self.audio_source_combobox = QComboBox()
147
+ self.audio_source_combobox.addItem(
148
+ language_wrapper.language_word_dict.get("pet_audio_speakers", "Speakers"), "speakers")
149
+ self.audio_source_combobox.addItem(
150
+ language_wrapper.language_word_dict.get("pet_audio_microphone", "Microphone"),
151
+ "microphone")
145
152
  self.audio_react_checkbox = QCheckBox(
146
153
  language_wrapper.language_word_dict.get("pet_audio_label", "React to audio"))
147
154
  self.drop_hint_label = QLabel(
@@ -185,6 +192,7 @@ class PetSettingUI(QWidget):
185
192
  self.grid_layout.addWidget(self.volume_label, 6, 2)
186
193
  self.grid_layout.addWidget(self.volume_combobox, 7, 2)
187
194
  self.grid_layout.addWidget(self.audio_react_checkbox, 7, 0)
195
+ self.grid_layout.addWidget(self.audio_source_combobox, 7, 1)
188
196
  self.grid_layout.addWidget(self.tag_checkbox, 7, 1)
189
197
  self.grid_layout.addWidget(self.speech_checkbox, 7, 2)
190
198
  self.grid_layout.addWidget(self.focus_label, 8, 0)
@@ -218,10 +226,7 @@ class PetSettingUI(QWidget):
218
226
  pet.set_chat_service(self.chat_service())
219
227
  geometry = self._target_geometry()
220
228
  if self.audio_react_checkbox.isChecked():
221
- # 跟隨寵物所在螢幕的音源(比對不到就用系統預設輸出裝置)
222
- # Follow the audio of the screen the pet lives on; default endpoint otherwise.
223
- pet.set_audio_level_provider(
224
- audio_level_provider_for_screen(geometry[1] if geometry is not None else None))
229
+ pet.set_audio_level_provider(self._audio_provider(geometry))
225
230
  pet.set_pet_window_flag()
226
231
  self.pet_list.append(pet)
227
232
  pet.show()
@@ -420,6 +425,20 @@ class PetSettingUI(QWidget):
420
425
  add_recent_file("pet", path)
421
426
  reload_recent_combobox(self.recent_files_combobox, "pet")
422
427
 
428
+ def _audio_provider(self, geometry):
429
+ """
430
+ 寵物要跟著什麼聲音動:喇叭(跟隨牠所在螢幕的輸出裝置)或麥克風
431
+ (你說話時牠才動)。兩者都只讀電表數值,不擷取音訊內容。
432
+ What the pet reacts to: the speakers on its own screen, or the
433
+ microphone so it only moves while you talk. Both read a meter value and
434
+ capture no audio content.
435
+ """
436
+ if self.audio_source_combobox.currentData() == "microphone":
437
+ return microphone_level
438
+ # 跟隨寵物所在螢幕的音源(比對不到就用系統預設輸出裝置)
439
+ # Follow the audio of the screen the pet lives on; default endpoint otherwise.
440
+ return audio_level_provider_for_screen(geometry[1] if geometry is not None else None)
441
+
423
442
  def get_state(self) -> dict:
424
443
  return {
425
444
  "pet_image_path": self.pet_image_path,
@@ -433,6 +452,7 @@ class PetSettingUI(QWidget):
433
452
  "target_monitor": self.target_monitor_combobox.currentText(),
434
453
  "volume": self.volume_combobox.currentText(),
435
454
  "audio_react": self.audio_react_checkbox.isChecked(),
455
+ "audio_source": self.audio_source_combobox.currentData(),
436
456
  "tag": self.tag_checkbox.isChecked(),
437
457
  "speech": self.speech_checkbox.isChecked(),
438
458
  "chat": self.chat_checkbox.isChecked(),
@@ -444,6 +464,7 @@ class PetSettingUI(QWidget):
444
464
  """把預設集的內容還原到這一頁的控制項上。"""
445
465
  self._restore_media(state)
446
466
  apply_combobox_data_state(self.behaviour_combobox, self._behaviour_from(state))
467
+ apply_combobox_data_state(self.audio_source_combobox, state.get("audio_source"))
447
468
  apply_combobox_text_state(state, (
448
469
  (self.size_combobox, "size"),
449
470
  (self.speed_combobox, "speed"),
@@ -11,9 +11,11 @@ from typing import List
11
11
  from PySide6.QtCore import Qt
12
12
  from PySide6.QtGui import QGuiApplication
13
13
  from PySide6.QtWidgets import (
14
+ QFileDialog,
14
15
  QCheckBox, QComboBox, QGridLayout, QLabel, QPushButton, QSlider, QWidget,
15
16
  )
16
17
 
18
+ from frontengine.show.canvas.whiteboard_widget import WhiteboardWidget
17
19
  from frontengine.show.presentation.annotation_overlay import (
18
20
  PEN_COLORS, TOOL_ERASER, TOOL_HIGHLIGHTER, TOOL_PEN, AnnotationOverlay,
19
21
  )
@@ -36,6 +38,7 @@ class PresentationSettingUI(QWidget):
36
38
  self.grid_layout.setContentsMargins(0, 0, 0, 0)
37
39
 
38
40
  self.annotation_widget_list: List[AnnotationOverlay] = []
41
+ self.whiteboard_widget_list: List[WhiteboardWidget] = []
39
42
  self.cursor_widget_list: List[CursorEffectWidget] = []
40
43
  self.keystroke_widget_list: List[KeystrokeDisplayWidget] = []
41
44
  self.magnifier_widget_list: List[MagnifierWidget] = []
@@ -109,6 +112,12 @@ class PresentationSettingUI(QWidget):
109
112
  self.magnifier_button.clicked.connect(self.toggle_magnifier)
110
113
 
111
114
  # Shared
115
+ self.whiteboard_button = QPushButton(
116
+ language_wrapper.language_word_dict.get("whiteboard_start", "Whiteboard"))
117
+ self.whiteboard_button.clicked.connect(self.toggle_whiteboard)
118
+ self.whiteboard_save_button = QPushButton(
119
+ language_wrapper.language_word_dict.get("whiteboard_save", "Save whiteboard"))
120
+ self.whiteboard_save_button.clicked.connect(self.save_whiteboard)
112
121
  self.target_monitor_label = QLabel(
113
122
  language_wrapper.language_word_dict.get("target_monitor_label", "Target monitor"))
114
123
  self.target_monitor_combobox = build_target_monitor_combobox()
@@ -136,9 +145,11 @@ class PresentationSettingUI(QWidget):
136
145
  self.grid_layout.addWidget(self.magnifier_label, 6, 0)
137
146
  self.grid_layout.addWidget(self.magnifier_zoom_combobox, 6, 1)
138
147
  self.grid_layout.addWidget(self.magnifier_button, 6, 2)
139
- self.grid_layout.addWidget(self.target_monitor_label, 7, 0)
140
- self.grid_layout.addWidget(self.target_monitor_combobox, 7, 1)
141
- self.grid_layout.addWidget(self.hint_label, 8, 0, 1, 3)
148
+ self.grid_layout.addWidget(self.whiteboard_button, 7, 0)
149
+ self.grid_layout.addWidget(self.whiteboard_save_button, 7, 1)
150
+ self.grid_layout.addWidget(self.target_monitor_label, 8, 0)
151
+ self.grid_layout.addWidget(self.target_monitor_combobox, 8, 1)
152
+ self.grid_layout.addWidget(self.hint_label, 9, 0, 1, 3)
142
153
 
143
154
  # --- shared ----------------------------------------------------------
144
155
  def target_screen(self):
@@ -331,6 +342,41 @@ class PresentationSettingUI(QWidget):
331
342
  self.magnifier_widget_list.remove(widget)
332
343
 
333
344
  # --- preset state ----------------------------------------------------
345
+ # --- whiteboard ------------------------------------------------------
346
+ def toggle_whiteboard(self) -> None:
347
+ if self.whiteboard_widget_list:
348
+ self.stop_whiteboard()
349
+ else:
350
+ self.start_whiteboard()
351
+
352
+ def start_whiteboard(self) -> None:
353
+ """開一塊無限畫布(可平移縮放,畫過的東西留在畫布座標上)。"""
354
+ front_engine_logger.info("[PresentationSettingUI] start_whiteboard")
355
+ board = WhiteboardWidget(self.annotate_color_combobox.currentData(),
356
+ self.annotate_width_slider.value())
357
+ self._present(board)
358
+ self.whiteboard_widget_list.append(board)
359
+ self.whiteboard_button.setText(
360
+ language_wrapper.language_word_dict.get("whiteboard_stop", "Close whiteboard"))
361
+
362
+ def stop_whiteboard(self) -> None:
363
+ self._close_all(self.whiteboard_widget_list)
364
+ self.whiteboard_button.setText(
365
+ language_wrapper.language_word_dict.get("whiteboard_start", "Whiteboard"))
366
+
367
+ def save_whiteboard(self):
368
+ """把白板存成圖片;沒畫東西就不存。"""
369
+ for board in self.whiteboard_widget_list[:]:
370
+ try:
371
+ target = QFileDialog.getSaveFileName(
372
+ self, language_wrapper.language_word_dict.get(
373
+ "whiteboard_save", "Save whiteboard"), "whiteboard.png", "PNG (*.png)")[0]
374
+ if target:
375
+ return board.save_image(target)
376
+ except RuntimeError:
377
+ self.whiteboard_widget_list.remove(board)
378
+ return None
379
+
334
380
  def get_state(self) -> dict:
335
381
  return {
336
382
  "annotate_tool": self.annotate_tool_combobox.currentData(),
@@ -54,6 +54,9 @@ user_setting_dict: Dict[str, Any] = {
54
54
  # Sticky note contents and positions, and whether to reopen them on launch.
55
55
  "sticky_notes": [],
56
56
  "sticky_notes_restore": False,
57
+ # 看板模式:依序輪播預設集(預設關閉)
58
+ # Signage mode: rotate through presets; off by default.
59
+ "signage": {"enabled": False, "presets": [], "interval_minutes": 5, "hide_window": True},
57
60
  # 手機遙控(會開一個本機網路埠,預設關閉)與 MIDI 綁定
58
61
  # The phone remote opens a local network port and is off by default;
59
62
  # plus the MIDI control bindings.