frontengine 1.0.45__tar.gz → 1.0.46__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {frontengine-1.0.45 → frontengine-1.0.46}/PKG-INFO +23 -1
- {frontengine-1.0.45 → frontengine-1.0.46}/README.md +22 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/base_widget.py +6 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/focus_shield/focus_shield_widget.py +3 -0
- frontengine-1.0.46/frontengine/ui/dialog/screen_privacy_dialog.py +98 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/main_ui.py +26 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/menu/settings_menu.py +7 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/control_center/control_center_ui.py +45 -2
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/user_setting/user_setting_file.py +3 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/multi_language/english.py +9 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/multi_language/traditional_chinese.py +9 -0
- frontengine-1.0.46/frontengine/utils/screen_privacy/capture_affinity.py +120 -0
- frontengine-1.0.46/frontengine/utils/screen_privacy/share_watch.py +128 -0
- frontengine-1.0.46/frontengine/worker/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine.egg-info/PKG-INFO +23 -1
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine.egg-info/SOURCES.txt +4 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/pyproject.toml +1 -1
- {frontengine-1.0.45 → frontengine-1.0.46}/LICENSE +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/__main__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/camera/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/camera/camera_widget.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/capture/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/capture/region_capture.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/focus_shield/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/gaming/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/gaming/crosshair_widget.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/gif/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/gif/paint_gif.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/image/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/image/paint_image.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/load/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/load/load_someone_make_ui.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/measure/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/measure/measure_widget.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/monitor/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/monitor/monitor_widget.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/notes/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/notes/sticky_note_widget.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/overlay_factory.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/particle/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/particle/particle_ui.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/pet/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/pet/desktop_pet.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/presentation/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/presentation/annotation_overlay.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/presentation/cursor_effects.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/presentation/keystroke_display.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/presentation/magnifier.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/scene/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/scene/extend_graphic_scene.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/scene/extend_graphic_view.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/scene/scene.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/screen_care/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/screen_care/color_vision_widget.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/screen_care/screen_filter.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/sound_player/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/sound_player/sound_effect.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/sound_player/sound_player.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/spectrum/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/spectrum/spectrum_widget.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/teleprompter/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/teleprompter/teleprompter_widget.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/text/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/text/draw_text.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/toast/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/toast/toast_widget.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/video/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/video/video_player.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/wallpaper/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/wallpaper/wallpaper_widget.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/web/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/web/webview.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/window_helpers.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/system_tray/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/system_tray/extend_system_tray.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/color/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/color/global_color.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/dialog/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/dialog/app_profile_dialog.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/dialog/choose_file_dialog.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/dialog/clipboard_dialog.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/dialog/hotkey_settings_dialog.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/dialog/reminder_dialog.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/dialog/save_file_dialog.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/dialog/screen_text_dialog.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/dialog/smart_pause_dialog.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/dialog/usage_report_dialog.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/dialog/window_pin_dialog.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/menu/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/menu/help_menu.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/menu/how_to_menu.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/menu/language_menu.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/menu/preset_menu.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/control_center/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/focus/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/focus/focus_setting_ui.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/gif/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/gif/gif_setting_ui.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/image/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/image/image_setting_ui.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/particle/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/particle/particle_setting_ui.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/pet/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/pet/pet_setting_ui.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/presentation/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/presentation/presentation_setting_ui.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/scene_setting/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/scene_setting/scene_manager.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/scene_setting/scene_page/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/scene_setting/scene_page/base_scene_page.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/scene_setting/scene_page/gif.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/scene_setting/scene_page/image.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/scene_setting/scene_page/registry.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/scene_setting/scene_page/sound.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/scene_setting/scene_page/text.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/scene_setting/scene_page/video.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/scene_setting/scene_page/web.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/scene_setting/scene_setting_ui.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/screen_care/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/screen_care/screen_care_setting_ui.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/sound_player/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/sound_player/sound_player_setting_ui.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/stream/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/stream/stream_setting_ui.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/text/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/text/text_setting_ui.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/tools/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/tools/tools_setting_ui.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/utils.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/video/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/video/video_setting_ui.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/wallpaper/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/wallpaper/wallpaper_setting_ui.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/web/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/web/web_setting_ui.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/widgets/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/widgets/widgets_setting_ui.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/user_setting/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/user_setting/preset_repository.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/user_setting/scene_setting.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/app_profile/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/app_profile/app_profile_service.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/audio_meter/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/audio_meter/audio_envelope.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/audio_meter/loopback_capture.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/audio_meter/screen_audio.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/audio_meter/spectrum_analyzer.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/audio_meter/system_audio_meter.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/autostart/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/autostart/autostart_service.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/break_reminder/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/break_reminder/break_reminder.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/browser/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/browser/browser.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/clipboard/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/clipboard/clipboard_history.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/clipboard/clipboard_watcher.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/color_vision/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/color_vision/color_vision.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/critical_exit/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/critical_exit/check_key_is_press.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/critical_exit/critical_exit.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/critical_exit/win32_vk.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/exception/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/exception/exception_tags.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/exception/exceptions.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/file/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/file/open/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/file/open/open_file.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/file/save/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/file/save/save_file.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/focus_shield/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/focus_shield/focus_shield.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/focus_timer/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/focus_timer/focus_timer.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/hotkey/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/hotkey/hotkey_service.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/input_watch/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/input_watch/input_watch_service.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/json/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/json/json_file.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/json/json_repository.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/json_format/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/json_format/json_process.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/logging/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/logging/loggin_instance.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/measure/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/measure/measure.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/multi_language/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/multi_language/france.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/multi_language/germany.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/multi_language/italy.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/multi_language/language_wrapper.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/multi_language/russian.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/multi_language/simplified_chinese.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/now_playing/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/now_playing/now_playing.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/obs/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/obs/obs_client.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/obs/obs_protocol.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/obs/websocket_frames.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/pet_chat/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/pet_chat/pet_chat_service.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/platform_info/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/platform_info/platform_info.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/playlist/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/playlist/playlist.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/plugins/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/plugins/plugin_loader.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/power_mode/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/power_mode/power_mode.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/preset_schedule/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/preset_schedule/preset_schedule_service.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/recording/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/recording/frame_recorder.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/recording/gif_writer.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/redirect_manager/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/redirect_manager/redirect_manager_class.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/reminder/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/reminder/reminder_service.py +0 -0
- {frontengine-1.0.45/frontengine/utils/screen_text → frontengine-1.0.46/frontengine/utils/screen_privacy}/__init__.py +0 -0
- {frontengine-1.0.45/frontengine/utils/smart_pause → frontengine-1.0.46/frontengine/utils/screen_text}/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/screen_text/screen_text_service.py +0 -0
- {frontengine-1.0.45/frontengine/utils/soundboard → frontengine-1.0.46/frontengine/utils/smart_pause}/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/smart_pause/pause_rules.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/smart_pause/smart_pause_service.py +0 -0
- {frontengine-1.0.45/frontengine/utils/system_stats → frontengine-1.0.46/frontengine/utils/soundboard}/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/soundboard/soundboard.py +0 -0
- {frontengine-1.0.45/frontengine/utils/text_source → frontengine-1.0.46/frontengine/utils/system_stats}/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/system_stats/system_stats.py +0 -0
- {frontengine-1.0.45/frontengine/utils/theme_schedule → frontengine-1.0.46/frontengine/utils/text_source}/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/text_source/text_source.py +0 -0
- {frontengine-1.0.45/frontengine/utils/usage_tracking → frontengine-1.0.46/frontengine/utils/theme_schedule}/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/theme_schedule/theme_schedule_service.py +0 -0
- {frontengine-1.0.45/frontengine/utils/weather → frontengine-1.0.46/frontengine/utils/usage_tracking}/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/usage_tracking/usage_service.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/usage_tracking/usage_tracker.py +0 -0
- {frontengine-1.0.45/frontengine/utils/window_pin → frontengine-1.0.46/frontengine/utils/weather}/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/weather/weather_service.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/web_url.py +0 -0
- {frontengine-1.0.45/frontengine/utils/workshop → frontengine-1.0.46/frontengine/utils/window_pin}/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/window_pin/window_layout.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/window_pin/window_pin.py +0 -0
- {frontengine-1.0.45/frontengine/worker → frontengine-1.0.46/frontengine/utils/workshop}/__init__.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/workshop/workshop_content.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine/worker/qthread_worker.py +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine.egg-info/dependency_links.txt +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine.egg-info/entry_points.txt +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine.egg-info/requires.txt +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/frontengine.egg-info/top_level.txt +0 -0
- {frontengine-1.0.45 → frontengine-1.0.46}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: frontengine
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.46
|
|
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
|
|
@@ -196,6 +196,28 @@ Four things on the **Stream** tab, for playing and for going live:
|
|
|
196
196
|
|
|
197
197
|
---
|
|
198
198
|
|
|
199
|
+
## Screen-sharing privacy
|
|
200
|
+
|
|
201
|
+
Your overlays are for you, not for the people you are sharing with. From
|
|
202
|
+
**Settings → Screen-sharing privacy**, FrontEngine can take them out of the
|
|
203
|
+
capture while a meeting app is open:
|
|
204
|
+
|
|
205
|
+
- **They stay on your own screen.** Only the captured copy is blank — this uses
|
|
206
|
+
Windows' `WDA_EXCLUDEFROMCAPTURE`, an OS-level flag that conferencing apps and
|
|
207
|
+
recorders honour.
|
|
208
|
+
- **Masks are the exception.** A distraction mask exists to cover something, so
|
|
209
|
+
it deliberately stays visible in the capture.
|
|
210
|
+
- **The trigger is your list.** Windows has no dependable "am I being captured"
|
|
211
|
+
API, so it watches for window titles you name — which also catches a meeting
|
|
212
|
+
held in a browser tab, where the executable is just the browser.
|
|
213
|
+
|
|
214
|
+
There is a manual **Hide from capture** button in the control center too.
|
|
215
|
+
|
|
216
|
+
> This is privacy, not security: it defeats the ordinary capture path, and it
|
|
217
|
+
> never hides anything from the person sitting at the desk. Windows only.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
199
221
|
## Screen time, clipboard and layouts
|
|
200
222
|
|
|
201
223
|
Three things that remember, all of them **off until you switch them on** and
|
|
@@ -172,6 +172,28 @@ Four things on the **Stream** tab, for playing and for going live:
|
|
|
172
172
|
|
|
173
173
|
---
|
|
174
174
|
|
|
175
|
+
## Screen-sharing privacy
|
|
176
|
+
|
|
177
|
+
Your overlays are for you, not for the people you are sharing with. From
|
|
178
|
+
**Settings → Screen-sharing privacy**, FrontEngine can take them out of the
|
|
179
|
+
capture while a meeting app is open:
|
|
180
|
+
|
|
181
|
+
- **They stay on your own screen.** Only the captured copy is blank — this uses
|
|
182
|
+
Windows' `WDA_EXCLUDEFROMCAPTURE`, an OS-level flag that conferencing apps and
|
|
183
|
+
recorders honour.
|
|
184
|
+
- **Masks are the exception.** A distraction mask exists to cover something, so
|
|
185
|
+
it deliberately stays visible in the capture.
|
|
186
|
+
- **The trigger is your list.** Windows has no dependable "am I being captured"
|
|
187
|
+
API, so it watches for window titles you name — which also catches a meeting
|
|
188
|
+
held in a browser tab, where the executable is just the browser.
|
|
189
|
+
|
|
190
|
+
There is a manual **Hide from capture** button in the control center too.
|
|
191
|
+
|
|
192
|
+
> This is privacy, not security: it defeats the ordinary capture path, and it
|
|
193
|
+
> never hides anything from the person sitting at the desk. Windows only.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
175
197
|
## Screen time, clipboard and layouts
|
|
176
198
|
|
|
177
199
|
Three things that remember, all of them **off until you switch them on** and
|
|
@@ -33,6 +33,12 @@ class BaseWidget(QWidget):
|
|
|
33
33
|
# 畫質檔位:影響更新頻率上限與算圖縮放(見 utils/power_mode)
|
|
34
34
|
# Quality tier: caps the refresh rate and scales rendering (see utils/power_mode).
|
|
35
35
|
self.quality_tier: str = DEFAULT_TIER
|
|
36
|
+
# 分享畫面時要不要一起藏起來。遮蔽用的覆蓋層要留在畫面上——
|
|
37
|
+
# 那正是它存在的理由,藏了就等於沒遮。
|
|
38
|
+
# Whether this hides along with the others while sharing. A masking
|
|
39
|
+
# overlay must stay visible in the capture: hiding it would defeat
|
|
40
|
+
# the only thing it is for.
|
|
41
|
+
self.keep_in_capture: bool = False
|
|
36
42
|
self._drag_origin = None
|
|
37
43
|
self._geometry_restored = False
|
|
38
44
|
|
{frontengine-1.0.45 → frontengine-1.0.46}/frontengine/show/focus_shield/focus_shield_widget.py
RENAMED
|
@@ -118,6 +118,9 @@ class DistractionMaskWidget(BaseWidget):
|
|
|
118
118
|
f"[DistractionMaskWidget] Init | region={region}, percent={percent}")
|
|
119
119
|
super().__init__()
|
|
120
120
|
self.opacity = 1.0
|
|
121
|
+
# 這一層是拿來遮住東西的,分享畫面時必須留在擷取結果裡
|
|
122
|
+
# This layer exists to cover something, so it stays in the capture.
|
|
123
|
+
self.keep_in_capture = True
|
|
121
124
|
self.region = region
|
|
122
125
|
self.percent = clamp_percent(percent)
|
|
123
126
|
self.mask_color = QColor(color) if QColor(color).isValid() else QColor("#101010")
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
螢幕分享隱私設定:偵測到會議程式時,自動把覆蓋層藏出擷取畫面。
|
|
3
|
+
|
|
4
|
+
Windows 沒有可靠的「我正在被擷取嗎」API,所以判斷依據是使用者自己列出的
|
|
5
|
+
會議程式有沒有開著視窗。這是刻意的:與其猜錯,不如讓使用者說了算。
|
|
6
|
+
|
|
7
|
+
Screen-sharing privacy: hide the overlays from the capture when a meeting app
|
|
8
|
+
turns up.
|
|
9
|
+
|
|
10
|
+
Windows offers no dependable "am I being captured" API, so the trigger is
|
|
11
|
+
whether one of the apps the user listed has a window open. That is deliberate -
|
|
12
|
+
better their list than our guess.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any, Dict, Optional
|
|
17
|
+
|
|
18
|
+
from PySide6.QtWidgets import (
|
|
19
|
+
QCheckBox, QDialog, QDialogButtonBox, QGridLayout, QLabel, QLineEdit, QWidget,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from frontengine.user_setting.user_setting_file import user_setting_dict, write_user_setting
|
|
23
|
+
from frontengine.utils.logging.loggin_instance import front_engine_logger
|
|
24
|
+
from frontengine.utils.multi_language.language_wrapper import language_wrapper
|
|
25
|
+
from frontengine.utils.screen_privacy import capture_affinity
|
|
26
|
+
from frontengine.utils.screen_privacy.share_watch import DEFAULT_SHARING_APPS
|
|
27
|
+
from frontengine.utils.smart_pause.pause_rules import parse_app_list
|
|
28
|
+
|
|
29
|
+
SETTING_KEY = "screen_share_privacy"
|
|
30
|
+
DEFAULT_SETTINGS: Dict[str, Any] = {"enabled": False, "apps": list(DEFAULT_SHARING_APPS)}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _t(key: str, fallback: str) -> str:
|
|
34
|
+
return language_wrapper.language_word_dict.get(key, fallback)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def current_settings() -> Dict[str, Any]:
|
|
38
|
+
"""目前的分享隱私設定(缺項目補預設值)。"""
|
|
39
|
+
stored = user_setting_dict.get(SETTING_KEY)
|
|
40
|
+
settings = dict(DEFAULT_SETTINGS)
|
|
41
|
+
if isinstance(stored, dict):
|
|
42
|
+
settings.update({key: stored[key] for key in DEFAULT_SETTINGS if key in stored})
|
|
43
|
+
settings["apps"] = parse_app_list(settings.get("apps")) or list(DEFAULT_SHARING_APPS)
|
|
44
|
+
return settings
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ScreenPrivacyDialog(QDialog):
|
|
48
|
+
"""設定哪些程式算是「正在分享」,以及要不要自動隱藏。"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, parent: Optional[QWidget] = None) -> None:
|
|
51
|
+
front_engine_logger.info("[ScreenPrivacyDialog] Init")
|
|
52
|
+
super().__init__(parent)
|
|
53
|
+
self.setWindowTitle(_t("screen_privacy_title", "Screen-sharing privacy"))
|
|
54
|
+
settings = current_settings()
|
|
55
|
+
|
|
56
|
+
self.enable_checkbox = QCheckBox(
|
|
57
|
+
_t("screen_privacy_enable", "Hide overlays while one of these apps is open"))
|
|
58
|
+
self.enable_checkbox.setChecked(bool(settings.get("enabled")))
|
|
59
|
+
self.apps_label = QLabel(_t("screen_privacy_apps_label", "Meeting and capture apps:"))
|
|
60
|
+
self.apps_edit = QLineEdit(", ".join(settings.get("apps", [])))
|
|
61
|
+
self.apps_edit.setPlaceholderText("zoom, teams, discord")
|
|
62
|
+
self.hint_label = QLabel(
|
|
63
|
+
_t("screen_privacy_hint",
|
|
64
|
+
"Matched against window titles, so a meeting held in a browser tab is caught "
|
|
65
|
+
"too. Your overlays stay on your own screen - only the capture is blank. "
|
|
66
|
+
"Masks stay visible, since covering something is what they are for.")
|
|
67
|
+
if capture_affinity.available() else
|
|
68
|
+
_t("screen_privacy_unsupported",
|
|
69
|
+
"Hiding overlays from screen capture is Windows only."))
|
|
70
|
+
self.hint_label.setWordWrap(True)
|
|
71
|
+
|
|
72
|
+
self.button_box = QDialogButtonBox(
|
|
73
|
+
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
|
74
|
+
self.button_box.accepted.connect(self.accept)
|
|
75
|
+
self.button_box.rejected.connect(self.reject)
|
|
76
|
+
|
|
77
|
+
layout = QGridLayout(self)
|
|
78
|
+
layout.addWidget(self.enable_checkbox, 0, 0, 1, 2)
|
|
79
|
+
layout.addWidget(self.apps_label, 1, 0, 1, 2)
|
|
80
|
+
layout.addWidget(self.apps_edit, 2, 0, 1, 2)
|
|
81
|
+
layout.addWidget(self.hint_label, 3, 0, 1, 2)
|
|
82
|
+
layout.addWidget(self.button_box, 4, 0, 1, 2)
|
|
83
|
+
for widget in (self.enable_checkbox, self.apps_edit):
|
|
84
|
+
widget.setEnabled(capture_affinity.available())
|
|
85
|
+
|
|
86
|
+
def settings(self) -> Dict[str, Any]:
|
|
87
|
+
"""對話框上目前的設定。"""
|
|
88
|
+
return {
|
|
89
|
+
"enabled": self.enable_checkbox.isChecked(),
|
|
90
|
+
"apps": parse_app_list(self.apps_edit.text()) or list(DEFAULT_SHARING_APPS),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
def accept(self) -> None:
|
|
94
|
+
settings = self.settings()
|
|
95
|
+
user_setting_dict[SETTING_KEY] = settings
|
|
96
|
+
write_user_setting()
|
|
97
|
+
front_engine_logger.info(f"[ScreenPrivacyDialog] saved | {settings}")
|
|
98
|
+
super().accept()
|
|
@@ -49,7 +49,9 @@ from frontengine.utils.logging.loggin_instance import front_engine_logger
|
|
|
49
49
|
from frontengine.utils.multi_language.language_wrapper import language_wrapper
|
|
50
50
|
from frontengine.utils.plugins.plugin_loader import load_plugins
|
|
51
51
|
from frontengine.utils.preset_schedule.preset_schedule_service import PresetScheduleService
|
|
52
|
+
from frontengine.ui.dialog.screen_privacy_dialog import current_settings as current_privacy_settings
|
|
52
53
|
from frontengine.ui.dialog.smart_pause_dialog import current_rules
|
|
54
|
+
from frontengine.utils.screen_privacy.share_watch import ShareWatchService
|
|
53
55
|
from frontengine.utils.app_profile.app_profile_service import AppProfileService
|
|
54
56
|
from frontengine.utils.clipboard.clipboard_history import DEFAULT_LIMIT, ClipboardHistory
|
|
55
57
|
from frontengine.utils.clipboard.clipboard_watcher import ClipboardWatcher
|
|
@@ -209,6 +211,12 @@ class FrontEngineMainUI(QMainWindow):
|
|
|
209
211
|
)
|
|
210
212
|
self.app_profile_service.start()
|
|
211
213
|
|
|
214
|
+
# 分享畫面時把覆蓋層藏出擷取結果(自己仍看得到)
|
|
215
|
+
# Hide the overlays from the capture while sharing; they stay on screen here.
|
|
216
|
+
self.share_watch_service = ShareWatchService(config_provider=current_privacy_settings)
|
|
217
|
+
self.share_watch_service.sharing_changed.connect(self._on_sharing_changed)
|
|
218
|
+
self.share_watch_service.start()
|
|
219
|
+
|
|
212
220
|
# 使用時間統計:預設關閉,只寫本機檔案,使用者可隨時清除
|
|
213
221
|
# Usage tracking: off by default, local file only, clearable at any time.
|
|
214
222
|
self.usage_tracker = UsageTracker(JsonRepository(Path(getcwd()) / USAGE_FILE))
|
|
@@ -460,6 +468,22 @@ class FrontEngineMainUI(QMainWindow):
|
|
|
460
468
|
self.control_center_ui.show_all()
|
|
461
469
|
self._smart_pause_hid = False
|
|
462
470
|
|
|
471
|
+
def _on_sharing_changed(self, sharing: bool, match: str) -> None:
|
|
472
|
+
"""
|
|
473
|
+
偵測到會議程式開著/關掉時,自動把覆蓋層藏出擷取結果或放回去。
|
|
474
|
+
遮蔽用的覆蓋層不受影響——它們本來就是要被看到的。
|
|
475
|
+
Hide the overlays from the capture when a meeting app appears, and put
|
|
476
|
+
them back when it goes. Masking overlays are left alone: being seen is
|
|
477
|
+
the whole point of them.
|
|
478
|
+
"""
|
|
479
|
+
front_engine_logger.info(
|
|
480
|
+
f"[MainUI] screen sharing={sharing}" + (f" | {match}" if match else ""))
|
|
481
|
+
self.control_center_ui.set_hide_from_capture(sharing)
|
|
482
|
+
if sharing:
|
|
483
|
+
self._last_toast = show_toast(
|
|
484
|
+
language_wrapper.language_word_dict.get(
|
|
485
|
+
"screen_privacy_toast", "Overlays hidden from screen capture"))
|
|
486
|
+
|
|
463
487
|
def _show_reminder(self, label: str) -> None:
|
|
464
488
|
"""提醒到期:在畫面上方顯示一張會自己消失的提示卡。"""
|
|
465
489
|
front_engine_logger.info(f"[MainUI] reminder | {label}")
|
|
@@ -526,6 +550,8 @@ class FrontEngineMainUI(QMainWindow):
|
|
|
526
550
|
self.tools_setting_ui.stop_camera()
|
|
527
551
|
if getattr(self, "stream_setting_ui", None) is not None:
|
|
528
552
|
self.stream_setting_ui.soundboard.stop_all()
|
|
553
|
+
if getattr(self, "share_watch_service", None) is not None:
|
|
554
|
+
self.share_watch_service.stop()
|
|
529
555
|
if getattr(self, "reminder_service", None) is not None:
|
|
530
556
|
self.reminder_service.stop()
|
|
531
557
|
if getattr(self, "app_profile_service", None) is not None:
|
|
@@ -10,6 +10,7 @@ from frontengine.ui.dialog.app_profile_dialog import AppProfileDialog
|
|
|
10
10
|
from frontengine.ui.dialog.clipboard_dialog import ClipboardDialog
|
|
11
11
|
from frontengine.ui.dialog.hotkey_settings_dialog import HotkeySettingsDialog
|
|
12
12
|
from frontengine.ui.dialog.reminder_dialog import ReminderDialog
|
|
13
|
+
from frontengine.ui.dialog.screen_privacy_dialog import ScreenPrivacyDialog
|
|
13
14
|
from frontengine.ui.dialog.smart_pause_dialog import SmartPauseDialog
|
|
14
15
|
from frontengine.ui.dialog.usage_report_dialog import UsageReportDialog
|
|
15
16
|
from frontengine.user_setting.user_setting_file import (
|
|
@@ -87,6 +88,12 @@ def build_settings_menu(ui: "FrontEngineMainUI") -> None:
|
|
|
87
88
|
menu.addAction(reminder_action)
|
|
88
89
|
ui.reminder_action = reminder_action
|
|
89
90
|
|
|
91
|
+
privacy_action = QAction(
|
|
92
|
+
_t("settings_menu_screen_privacy", "Screen-sharing privacy..."), menu)
|
|
93
|
+
privacy_action.triggered.connect(lambda: _open_dialog(ui, ScreenPrivacyDialog))
|
|
94
|
+
menu.addAction(privacy_action)
|
|
95
|
+
ui.screen_privacy_action = privacy_action
|
|
96
|
+
|
|
90
97
|
usage_action = QAction(_t("settings_menu_usage", "Screen time..."), menu)
|
|
91
98
|
usage_action.triggered.connect(lambda: _open_usage_dialog(ui))
|
|
92
99
|
menu.addAction(usage_action)
|
{frontengine-1.0.45 → frontengine-1.0.46}/frontengine/ui/page/control_center/control_center_ui.py
RENAMED
|
@@ -16,6 +16,7 @@ from frontengine.ui.page.web.web_setting_ui import WEBSettingUI
|
|
|
16
16
|
from frontengine.user_setting.user_setting_file import clear_overlay_geometry
|
|
17
17
|
from frontengine.utils.logging.loggin_instance import front_engine_logger
|
|
18
18
|
from frontengine.utils.multi_language.language_wrapper import language_wrapper
|
|
19
|
+
from frontengine.utils.screen_privacy import capture_affinity
|
|
19
20
|
from frontengine.utils.power_mode.power_mode import (
|
|
20
21
|
TIER_BALANCED, TIER_HIGH, TIER_SAVER, normalize_tier,
|
|
21
22
|
)
|
|
@@ -94,6 +95,10 @@ class ControlCenterUI(QWidget):
|
|
|
94
95
|
self._extra_overlay_sources: list = []
|
|
95
96
|
self.low_power_button = self._create_button(
|
|
96
97
|
"control_center_low_power_on", self.toggle_low_power)
|
|
98
|
+
self._hidden_from_capture = False
|
|
99
|
+
self.capture_button = self._create_button(
|
|
100
|
+
"control_center_hide_from_capture", self.toggle_hide_from_capture)
|
|
101
|
+
self.capture_button.setEnabled(capture_affinity.available())
|
|
97
102
|
self.clear_all_button = self._create_button("control_center_close_all", self.clear_all)
|
|
98
103
|
|
|
99
104
|
# 畫質檔位:比省電模式更細,一次套用到所有覆蓋層
|
|
@@ -135,8 +140,9 @@ class ControlCenterUI(QWidget):
|
|
|
135
140
|
self.grid_layout.addWidget(self.low_power_button, 13, 0)
|
|
136
141
|
self.grid_layout.addWidget(self.quality_label, 14, 0)
|
|
137
142
|
self.grid_layout.addWidget(self.quality_combobox, 15, 0)
|
|
138
|
-
self.grid_layout.addWidget(self.
|
|
139
|
-
self.grid_layout.addWidget(self.
|
|
143
|
+
self.grid_layout.addWidget(self.capture_button, 16, 0)
|
|
144
|
+
self.grid_layout.addWidget(self.clear_all_button, 17, 0)
|
|
145
|
+
self.grid_layout.addWidget(self.log_panel_scroll_area, 0, 1, 18, 1) # rowSpan covers every button
|
|
140
146
|
self.setLayout(self.grid_layout)
|
|
141
147
|
|
|
142
148
|
# Redirect
|
|
@@ -353,6 +359,43 @@ class ControlCenterUI(QWidget):
|
|
|
353
359
|
)
|
|
354
360
|
)
|
|
355
361
|
|
|
362
|
+
def set_hide_from_capture(self, hidden: bool) -> int:
|
|
363
|
+
"""
|
|
364
|
+
分享畫面時把覆蓋層藏起來(自己看得到、對方看不到)。標了
|
|
365
|
+
keep_in_capture 的覆蓋層不會被藏——遮蔽層本來就是要被看到的。
|
|
366
|
+
Hide the overlays from screen capture: still visible here, blank for the
|
|
367
|
+
people you are sharing with. Overlays marked keep_in_capture are left
|
|
368
|
+
alone, since a mask is meant to be seen.
|
|
369
|
+
"""
|
|
370
|
+
self._hidden_from_capture = bool(hidden)
|
|
371
|
+
applied = 0
|
|
372
|
+
|
|
373
|
+
def apply(widget) -> None:
|
|
374
|
+
nonlocal applied
|
|
375
|
+
if getattr(widget, "keep_in_capture", False):
|
|
376
|
+
capture_affinity.apply_to_widget(widget, False)
|
|
377
|
+
return
|
|
378
|
+
if capture_affinity.apply_to_widget(widget, self._hidden_from_capture):
|
|
379
|
+
applied += 1
|
|
380
|
+
|
|
381
|
+
self._for_each_overlay(apply)
|
|
382
|
+
front_engine_logger.info(
|
|
383
|
+
f"ControlCenterUI set_hide_from_capture | hidden={hidden}, applied={applied}")
|
|
384
|
+
self.capture_button.setText(
|
|
385
|
+
language_wrapper.language_word_dict.get(
|
|
386
|
+
"control_center_show_in_capture" if self._hidden_from_capture
|
|
387
|
+
else "control_center_hide_from_capture",
|
|
388
|
+
"Show in capture" if self._hidden_from_capture else "Hide from capture"))
|
|
389
|
+
return applied
|
|
390
|
+
|
|
391
|
+
def toggle_hide_from_capture(self) -> None:
|
|
392
|
+
"""切換分享時是否隱藏覆蓋層。"""
|
|
393
|
+
self.set_hide_from_capture(not self._hidden_from_capture)
|
|
394
|
+
|
|
395
|
+
@property
|
|
396
|
+
def hidden_from_capture(self) -> bool:
|
|
397
|
+
return self._hidden_from_capture
|
|
398
|
+
|
|
356
399
|
def toggle_low_power(self) -> None:
|
|
357
400
|
"""切換省電模式 / Toggle low-power mode."""
|
|
358
401
|
self.set_low_power(not getattr(self, "_low_power", False))
|
|
@@ -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
|
+
# Hide overlays from the capture while sharing; off by default.
|
|
59
|
+
"screen_share_privacy": {"enabled": False, "apps": []},
|
|
57
60
|
# 使用時間統計(只寫本機檔案、預設關閉)
|
|
58
61
|
# Usage tracking: local file only, off by default.
|
|
59
62
|
"usage_tracking": False,
|
|
@@ -542,4 +542,13 @@ english_word_dict = {
|
|
|
542
542
|
"stream_obs_record": 'Toggle recording',
|
|
543
543
|
"stream_obs_stream": 'Toggle streaming',
|
|
544
544
|
"stream_hint": 'The crosshair is only drawn on top - no game is modified. OBS control uses its own WebSocket; the password is kept in your local settings.',
|
|
545
|
+
"control_center_hide_from_capture": 'Hide from capture',
|
|
546
|
+
"control_center_show_in_capture": 'Show in capture',
|
|
547
|
+
"settings_menu_screen_privacy": 'Screen-sharing privacy...',
|
|
548
|
+
"screen_privacy_title": 'Screen-sharing privacy',
|
|
549
|
+
"screen_privacy_enable": 'Hide overlays while one of these apps is open',
|
|
550
|
+
"screen_privacy_apps_label": 'Meeting and capture apps:',
|
|
551
|
+
"screen_privacy_hint": 'Matched against window titles, so a meeting held in a browser tab is caught too. Your overlays stay on your own screen - only the capture is blank. Masks stay visible, since covering something is what they are for.',
|
|
552
|
+
"screen_privacy_unsupported": 'Hiding overlays from screen capture is Windows only.',
|
|
553
|
+
"screen_privacy_toast": 'Overlays hidden from screen capture',
|
|
545
554
|
}
|
{frontengine-1.0.45 → frontengine-1.0.46}/frontengine/utils/multi_language/traditional_chinese.py
RENAMED
|
@@ -541,4 +541,13 @@ traditional_chinese_word_dict = {
|
|
|
541
541
|
"stream_obs_record": '切換錄影',
|
|
542
542
|
"stream_obs_stream": '切換直播',
|
|
543
543
|
"stream_hint": '準星只是疊在畫面上的一層,不會更動任何遊戲。OBS 控制走它自己的 WebSocket,密碼只留在本機設定檔裡。',
|
|
544
|
+
"control_center_hide_from_capture": '分享時隱藏',
|
|
545
|
+
"control_center_show_in_capture": '分享時顯示',
|
|
546
|
+
"settings_menu_screen_privacy": '螢幕分享隱私…',
|
|
547
|
+
"screen_privacy_title": '螢幕分享隱私',
|
|
548
|
+
"screen_privacy_enable": '這些程式開著時,把覆蓋層藏出擷取畫面',
|
|
549
|
+
"screen_privacy_apps_label": '會議與擷取程式:',
|
|
550
|
+
"screen_privacy_hint": '比對的是視窗標題,所以在瀏覽器分頁裡開的會議也抓得到。覆蓋層仍會顯示在你自己的螢幕上,只有擷取畫面那一份是空的。遮蔽層不會被藏起來——遮住東西正是它存在的理由。',
|
|
551
|
+
"screen_privacy_unsupported": '把覆蓋層藏出螢幕擷取僅支援 Windows。',
|
|
552
|
+
"screen_privacy_toast": '已把覆蓋層藏出螢幕擷取',
|
|
544
553
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
把視窗排除在螢幕擷取之外:分享畫面時,自己看得到覆蓋層,對方看不到。
|
|
3
|
+
|
|
4
|
+
用的是 Win32 的 `SetWindowDisplayAffinity(WDA_EXCLUDEFROMCAPTURE)`。這是作業系統
|
|
5
|
+
層級的旗標——會議軟體、錄影軟體、螢幕擷取 API 拿到的畫面裡那塊會是空的,
|
|
6
|
+
而本機螢幕上照常顯示。僅 Windows 10 2004 以後有效,其他平台一律回報做不到。
|
|
7
|
+
|
|
8
|
+
**這是「隱私」不是「保全」**:它擋的是正常的擷取管道,不是防止有心人用其他
|
|
9
|
+
方式取得畫面,也不會讓視窗對使用者自己隱形。
|
|
10
|
+
|
|
11
|
+
Keep a window out of screen captures: you still see your overlays while the
|
|
12
|
+
people you are sharing with do not.
|
|
13
|
+
|
|
14
|
+
This is Win32's `SetWindowDisplayAffinity(WDA_EXCLUDEFROMCAPTURE)` - an
|
|
15
|
+
OS-level flag, so conferencing apps, recorders and capture APIs all receive
|
|
16
|
+
blank pixels there while the local display is unchanged. Windows 10 2004 and
|
|
17
|
+
later; everywhere else it reports that it cannot be done.
|
|
18
|
+
|
|
19
|
+
**This is privacy, not security**: it defeats the ordinary capture path, not a
|
|
20
|
+
determined attacker, and it never hides the window from the person at the desk.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import sys
|
|
25
|
+
from typing import Optional
|
|
26
|
+
|
|
27
|
+
from frontengine.utils.logging.loggin_instance import front_engine_logger
|
|
28
|
+
|
|
29
|
+
# WDA_NONE:正常擷取;WDA_EXCLUDEFROMCAPTURE:擷取時整片留白
|
|
30
|
+
# WDA_NONE captures normally; WDA_EXCLUDEFROMCAPTURE comes out blank.
|
|
31
|
+
WDA_NONE = 0x00000000
|
|
32
|
+
WDA_EXCLUDEFROMCAPTURE = 0x00000011
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def available() -> bool:
|
|
36
|
+
"""這個平台能不能控制擷取可見性。"""
|
|
37
|
+
return sys.platform == "win32"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _user32():
|
|
41
|
+
"""拿到宣告好 argtypes 的 user32;非 Windows 回傳 None。"""
|
|
42
|
+
if not available():
|
|
43
|
+
return None
|
|
44
|
+
import ctypes
|
|
45
|
+
from ctypes import wintypes
|
|
46
|
+
|
|
47
|
+
user32 = ctypes.windll.user32
|
|
48
|
+
user32.SetWindowDisplayAffinity.argtypes = [wintypes.HWND, wintypes.DWORD]
|
|
49
|
+
user32.SetWindowDisplayAffinity.restype = wintypes.BOOL
|
|
50
|
+
user32.GetWindowDisplayAffinity.argtypes = [wintypes.HWND, ctypes.POINTER(wintypes.DWORD)]
|
|
51
|
+
user32.GetWindowDisplayAffinity.restype = wintypes.BOOL
|
|
52
|
+
return user32
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def set_excluded_from_capture(handle: int, excluded: bool) -> bool:
|
|
56
|
+
"""
|
|
57
|
+
設定某個視窗要不要被排除在擷取之外;成功回傳 True。
|
|
58
|
+
Set whether a window is excluded from capture; True when it took effect.
|
|
59
|
+
"""
|
|
60
|
+
user32 = _user32()
|
|
61
|
+
if user32 is None or not handle:
|
|
62
|
+
return False
|
|
63
|
+
try:
|
|
64
|
+
from ctypes import wintypes
|
|
65
|
+
|
|
66
|
+
affinity = WDA_EXCLUDEFROMCAPTURE if excluded else WDA_NONE
|
|
67
|
+
result = user32.SetWindowDisplayAffinity(wintypes.HWND(int(handle)), affinity)
|
|
68
|
+
front_engine_logger.debug(
|
|
69
|
+
f"[CaptureAffinity] {handle} excluded={excluded} ok={bool(result)}")
|
|
70
|
+
return bool(result)
|
|
71
|
+
except Exception as error: # pragma: no cover - Win32 boundary
|
|
72
|
+
front_engine_logger.warning(f"[CaptureAffinity] failed: {error!r}")
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def is_excluded_from_capture(handle: int) -> Optional[bool]:
|
|
77
|
+
"""某個視窗目前是不是被排除在擷取之外;問不到回傳 None。"""
|
|
78
|
+
user32 = _user32()
|
|
79
|
+
if user32 is None or not handle:
|
|
80
|
+
return None
|
|
81
|
+
try:
|
|
82
|
+
import ctypes
|
|
83
|
+
from ctypes import wintypes
|
|
84
|
+
|
|
85
|
+
affinity = wintypes.DWORD()
|
|
86
|
+
if not user32.GetWindowDisplayAffinity(wintypes.HWND(int(handle)),
|
|
87
|
+
ctypes.byref(affinity)):
|
|
88
|
+
return None
|
|
89
|
+
return affinity.value == WDA_EXCLUDEFROMCAPTURE
|
|
90
|
+
except Exception as error: # pragma: no cover - Win32 boundary
|
|
91
|
+
front_engine_logger.debug(f"[CaptureAffinity] read failed: {error!r}")
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def apply_to_widget(widget, excluded: bool) -> bool:
|
|
96
|
+
"""
|
|
97
|
+
對一個 Qt widget 套用擷取可見性。視窗還沒建立原生 handle 時回傳 False,
|
|
98
|
+
呼叫端可以等 show() 之後再試一次。
|
|
99
|
+
Apply capture visibility to a Qt widget. False when it has no native handle
|
|
100
|
+
yet, so the caller can try again after show().
|
|
101
|
+
"""
|
|
102
|
+
if widget is None:
|
|
103
|
+
return False
|
|
104
|
+
try:
|
|
105
|
+
handle = int(widget.winId())
|
|
106
|
+
except (RuntimeError, ValueError): # pragma: no cover - widget torn down
|
|
107
|
+
return False
|
|
108
|
+
return set_excluded_from_capture(handle, excluded)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def apply_to_widgets(widgets, excluded: bool) -> int:
|
|
112
|
+
"""對一批 widget 套用,回傳成功的數量。"""
|
|
113
|
+
applied = 0
|
|
114
|
+
for widget in list(widgets or ()):
|
|
115
|
+
try:
|
|
116
|
+
if apply_to_widget(widget, excluded):
|
|
117
|
+
applied += 1
|
|
118
|
+
except RuntimeError: # pragma: no cover - widget closed mid-loop
|
|
119
|
+
continue
|
|
120
|
+
return applied
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
偵測「可能正在分享畫面」:看使用者指定的會議程式在不在。
|
|
3
|
+
|
|
4
|
+
Windows 沒有可靠的「我現在被擷取了嗎」API,所以這裡用的是誠實的近似:
|
|
5
|
+
使用者自己列出會議軟體(Zoom、Teams、Discord…),只要那些程式有開著視窗,
|
|
6
|
+
就當作可能在分享。這是刻意選擇的做法——與其猜錯,不如讓使用者說了算。
|
|
7
|
+
|
|
8
|
+
Spot "probably sharing the screen" by watching for the meeting apps the user
|
|
9
|
+
named.
|
|
10
|
+
|
|
11
|
+
Windows has no dependable "am I being captured" API, so this is an honest
|
|
12
|
+
approximation: the user lists their conferencing apps, and any of them having a
|
|
13
|
+
window open counts as possibly sharing. That is deliberate - better to let the
|
|
14
|
+
user decide than to guess wrong.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
from PySide6.QtCore import QObject, QTimer, Signal
|
|
21
|
+
|
|
22
|
+
from frontengine.utils.logging.loggin_instance import front_engine_logger
|
|
23
|
+
from frontengine.utils.smart_pause.pause_rules import normalize_app_name, parse_app_list
|
|
24
|
+
from frontengine.utils.window_pin.window_pin import list_windows
|
|
25
|
+
|
|
26
|
+
# 常見的會議/串流程式,當作預設值;使用者可以自己改
|
|
27
|
+
# The usual conferencing and streaming apps, as a starting point the user edits.
|
|
28
|
+
DEFAULT_SHARING_APPS = ("zoom", "teams", "ms-teams", "discord", "obs64", "obs",
|
|
29
|
+
"slack", "webex", "gotomeeting")
|
|
30
|
+
DEFAULT_INTERVAL_MS = 3000
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def running_app_names(lister: Optional[Callable[[], List]] = None) -> List[str]:
|
|
34
|
+
"""
|
|
35
|
+
目前有視窗的程式名稱(正規化過)。用視窗列舉而不是列舉所有程序:
|
|
36
|
+
看得見的視窗才可能被分享,也不需要更高的權限。
|
|
37
|
+
The normalized names of apps that currently have a window. Window
|
|
38
|
+
enumeration rather than a full process list: only visible windows can be
|
|
39
|
+
shared, and it needs no extra privileges.
|
|
40
|
+
"""
|
|
41
|
+
names = []
|
|
42
|
+
for _handle, title in (lister or list_windows)() or []:
|
|
43
|
+
name = normalize_app_name(title)
|
|
44
|
+
if name and name not in names:
|
|
45
|
+
names.append(name)
|
|
46
|
+
return names
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def sharing_app_running(watched: Any, window_titles: List[str]) -> Optional[str]:
|
|
50
|
+
"""
|
|
51
|
+
有沒有被盯著的程式開著視窗。比對是「視窗標題裡含有那個名字」——會議軟體
|
|
52
|
+
的標題通常帶著自己的名字(例如 "Zoom Meeting"),比對執行檔名反而抓不到
|
|
53
|
+
瀏覽器裡開的會議。回傳第一個命中的名字,沒有就 None。
|
|
54
|
+
Whether one of the watched apps has a window. Matching is "the title
|
|
55
|
+
contains the name", because meeting windows usually carry their app's name
|
|
56
|
+
("Zoom Meeting") while an executable-name match would miss a meeting held
|
|
57
|
+
in a browser tab. Returns the first hit, or None.
|
|
58
|
+
"""
|
|
59
|
+
wanted = parse_app_list(watched) or list(DEFAULT_SHARING_APPS)
|
|
60
|
+
haystack = [title.lower() for title in window_titles]
|
|
61
|
+
for name in wanted:
|
|
62
|
+
if any(name in title for title in haystack):
|
|
63
|
+
return name
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ShareWatchService(QObject):
|
|
68
|
+
"""
|
|
69
|
+
定時檢查有沒有會議程式開著,狀態改變時發出訊號。視窗來源與設定都可注入。
|
|
70
|
+
Poll for a conferencing app and signal when that changes. Both the window
|
|
71
|
+
source and the configuration are injectable.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
sharing_changed = Signal(bool, str) # (可能在分享, 命中的程式名稱)
|
|
75
|
+
|
|
76
|
+
def __init__(self, config_provider: Optional[Callable[[], Dict[str, Any]]] = None,
|
|
77
|
+
window_provider: Optional[Callable[[], List]] = None,
|
|
78
|
+
interval_ms: int = DEFAULT_INTERVAL_MS,
|
|
79
|
+
parent: Optional[QObject] = None) -> None:
|
|
80
|
+
super().__init__(parent)
|
|
81
|
+
self._config_provider = config_provider or (lambda: {})
|
|
82
|
+
self._window_provider = window_provider or list_windows
|
|
83
|
+
self._sharing = False
|
|
84
|
+
self._match = ""
|
|
85
|
+
self._timer = QTimer(self)
|
|
86
|
+
self._timer.setInterval(max(1000, int(interval_ms)))
|
|
87
|
+
self._timer.timeout.connect(self.poll_once)
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def sharing(self) -> bool:
|
|
91
|
+
return self._sharing
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def match(self) -> str:
|
|
95
|
+
"""目前是哪個程式讓它判斷成「在分享」。"""
|
|
96
|
+
return self._match
|
|
97
|
+
|
|
98
|
+
def start(self) -> None:
|
|
99
|
+
front_engine_logger.info("[ShareWatch] start")
|
|
100
|
+
self._timer.start()
|
|
101
|
+
|
|
102
|
+
def stop(self) -> None:
|
|
103
|
+
front_engine_logger.info("[ShareWatch] stop")
|
|
104
|
+
self._timer.stop()
|
|
105
|
+
|
|
106
|
+
def poll_once(self) -> None:
|
|
107
|
+
"""檢查一次(測試會直接呼叫)。"""
|
|
108
|
+
try:
|
|
109
|
+
config = self._config_provider() or {}
|
|
110
|
+
if not config.get("enabled", False):
|
|
111
|
+
self._update(False, "")
|
|
112
|
+
return
|
|
113
|
+
titles = [str(title) for _handle, title in (self._window_provider() or [])]
|
|
114
|
+
match = sharing_app_running(config.get("apps"), titles)
|
|
115
|
+
except Exception as error: # pragma: no cover - defensive boundary
|
|
116
|
+
front_engine_logger.warning(f"[ShareWatch] poll error: {error!r}")
|
|
117
|
+
return
|
|
118
|
+
self._update(match is not None, match or "")
|
|
119
|
+
|
|
120
|
+
def _update(self, sharing: bool, match: str) -> None:
|
|
121
|
+
if sharing == self._sharing:
|
|
122
|
+
self._match = match
|
|
123
|
+
return
|
|
124
|
+
self._sharing = sharing
|
|
125
|
+
self._match = match
|
|
126
|
+
front_engine_logger.info(
|
|
127
|
+
f"[ShareWatch] sharing={sharing}" + (f" | {match}" if match else ""))
|
|
128
|
+
self.sharing_changed.emit(sharing, match)
|
|
File without changes
|