frontengine 1.0.26__tar.gz → 1.0.27__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.26 → frontengine-1.0.27}/PKG-INFO +28 -1
- {frontengine-1.0.26 → frontengine-1.0.27}/README.md +27 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/show/pet/desktop_pet.py +103 -5
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/pet/pet_setting_ui.py +13 -3
- frontengine-1.0.27/frontengine/utils/audio_meter/audio_envelope.py +56 -0
- frontengine-1.0.27/frontengine/utils/audio_meter/screen_audio.py +130 -0
- frontengine-1.0.27/frontengine/utils/audio_meter/system_audio_meter.py +298 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/multi_language/english.py +3 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/multi_language/traditional_chinese.py +2 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine.egg-info/PKG-INFO +28 -1
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine.egg-info/SOURCES.txt +2 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/pyproject.toml +1 -1
- frontengine-1.0.26/frontengine/utils/audio_meter/system_audio_meter.py +0 -136
- {frontengine-1.0.26 → frontengine-1.0.27}/LICENSE +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/__main__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/show/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/show/base_widget.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/show/gif/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/show/gif/paint_gif.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/show/image/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/show/image/paint_image.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/show/load/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/show/load/load_someone_make_ui.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/show/overlay_factory.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/show/particle/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/show/particle/particle_ui.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/show/pet/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/show/scene/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/show/scene/extend_graphic_scene.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/show/scene/extend_graphic_view.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/show/scene/scene.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/show/sound_player/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/show/sound_player/sound_effect.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/show/sound_player/sound_player.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/show/text/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/show/text/draw_text.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/show/video/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/show/video/video_player.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/show/web/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/show/web/webview.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/show/window_helpers.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/system_tray/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/system_tray/extend_system_tray.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/color/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/color/global_color.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/dialog/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/dialog/choose_file_dialog.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/dialog/hotkey_settings_dialog.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/dialog/save_file_dialog.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/main_ui.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/menu/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/menu/help_menu.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/menu/how_to_menu.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/menu/language_menu.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/menu/preset_menu.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/menu/settings_menu.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/control_center/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/control_center/control_center_ui.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/gif/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/gif/gif_setting_ui.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/image/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/image/image_setting_ui.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/particle/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/particle/particle_setting_ui.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/pet/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/scene_setting/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/scene_setting/scene_manager.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/scene_setting/scene_page/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/scene_setting/scene_page/base_scene_page.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/scene_setting/scene_page/gif.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/scene_setting/scene_page/image.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/scene_setting/scene_page/registry.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/scene_setting/scene_page/sound.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/scene_setting/scene_page/text.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/scene_setting/scene_page/video.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/scene_setting/scene_page/web.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/scene_setting/scene_setting_ui.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/sound_player/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/sound_player/sound_player_setting_ui.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/text/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/text/text_setting_ui.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/utils.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/video/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/video/video_setting_ui.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/web/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/ui/page/web/web_setting_ui.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/user_setting/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/user_setting/preset_repository.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/user_setting/scene_setting.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/user_setting/user_setting_file.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/audio_meter/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/browser/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/browser/browser.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/critical_exit/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/critical_exit/check_key_is_press.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/critical_exit/critical_exit.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/critical_exit/win32_vk.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/exception/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/exception/exception_tags.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/exception/exceptions.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/file/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/file/open/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/file/open/open_file.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/file/save/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/file/save/save_file.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/hotkey/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/hotkey/hotkey_service.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/json/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/json/json_file.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/json/json_repository.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/json_format/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/json_format/json_process.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/logging/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/logging/loggin_instance.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/multi_language/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/multi_language/france.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/multi_language/germany.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/multi_language/italy.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/multi_language/language_wrapper.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/multi_language/russian.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/multi_language/simplified_chinese.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/preset_schedule/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/preset_schedule/preset_schedule_service.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/redirect_manager/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/redirect_manager/redirect_manager_class.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/smart_pause/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/smart_pause/smart_pause_service.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/theme_schedule/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/theme_schedule/theme_schedule_service.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/utils/web_url.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/worker/__init__.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine/worker/qthread_worker.py +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine.egg-info/dependency_links.txt +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine.egg-info/entry_points.txt +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine.egg-info/requires.txt +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/frontengine.egg-info/top_level.txt +0 -0
- {frontengine-1.0.26 → frontengine-1.0.27}/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.27
|
|
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
|
|
@@ -53,6 +53,33 @@ It provides an intuitive interface and supports multiple media formats for inter
|
|
|
53
53
|
|
|
54
54
|
---
|
|
55
55
|
|
|
56
|
+
## Desktop pet
|
|
57
|
+
|
|
58
|
+
Spawn an animated sprite that lives on your desktop, from the **Pet** tab.
|
|
59
|
+
|
|
60
|
+
- **Sprites** — pick a single GIF/WebP/PNG, or a *pet pack* folder whose file
|
|
61
|
+
names map to states: `walk`, `idle`, `sleep`, `climb`, `fall`, `drag`
|
|
62
|
+
(missing states fall back to `walk`).
|
|
63
|
+
- **Behaviour** — walk on the floor with gravity (throw it and it bounces),
|
|
64
|
+
wander freely, or chase the cursor. Floor pets can climb screen edges and
|
|
65
|
+
stand on the top edge of other windows.
|
|
66
|
+
- **Life** — mood, fullness and an affection level that persist between runs;
|
|
67
|
+
the pet grows as it levels up, chats in speech bubbles, naps while you are
|
|
68
|
+
away, and warns you about a low battery.
|
|
69
|
+
- **Interaction** — drag it around, right-click to clone/feed/set a reminder,
|
|
70
|
+
and **drop a file onto it**: an image or pet pack becomes its new look,
|
|
71
|
+
anything else is eaten as a snack.
|
|
72
|
+
- **React to audio** — the pet pulses to your speakers' output level. It reads
|
|
73
|
+
only the Windows output *meter* (WASAPI `IAudioMeterInformation`); no audio
|
|
74
|
+
is captured or recorded. Peaks are smoothed with an RMS window and a
|
|
75
|
+
fast-attack/slow-decay envelope so the pulse breathes instead of flickering.
|
|
76
|
+
On multi-monitor setups each pet follows the audio endpoint that matches its
|
|
77
|
+
own screen, falling back to the default output device.
|
|
78
|
+
|
|
79
|
+
Audio features are Windows-only and degrade to "no pulse" elsewhere.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
56
83
|
## Install
|
|
57
84
|
|
|
58
85
|
- **System Requirements**
|
|
@@ -29,6 +29,33 @@ It provides an intuitive interface and supports multiple media formats for inter
|
|
|
29
29
|
|
|
30
30
|
---
|
|
31
31
|
|
|
32
|
+
## Desktop pet
|
|
33
|
+
|
|
34
|
+
Spawn an animated sprite that lives on your desktop, from the **Pet** tab.
|
|
35
|
+
|
|
36
|
+
- **Sprites** — pick a single GIF/WebP/PNG, or a *pet pack* folder whose file
|
|
37
|
+
names map to states: `walk`, `idle`, `sleep`, `climb`, `fall`, `drag`
|
|
38
|
+
(missing states fall back to `walk`).
|
|
39
|
+
- **Behaviour** — walk on the floor with gravity (throw it and it bounces),
|
|
40
|
+
wander freely, or chase the cursor. Floor pets can climb screen edges and
|
|
41
|
+
stand on the top edge of other windows.
|
|
42
|
+
- **Life** — mood, fullness and an affection level that persist between runs;
|
|
43
|
+
the pet grows as it levels up, chats in speech bubbles, naps while you are
|
|
44
|
+
away, and warns you about a low battery.
|
|
45
|
+
- **Interaction** — drag it around, right-click to clone/feed/set a reminder,
|
|
46
|
+
and **drop a file onto it**: an image or pet pack becomes its new look,
|
|
47
|
+
anything else is eaten as a snack.
|
|
48
|
+
- **React to audio** — the pet pulses to your speakers' output level. It reads
|
|
49
|
+
only the Windows output *meter* (WASAPI `IAudioMeterInformation`); no audio
|
|
50
|
+
is captured or recorded. Peaks are smoothed with an RMS window and a
|
|
51
|
+
fast-attack/slow-decay envelope so the pulse breathes instead of flickering.
|
|
52
|
+
On multi-monitor setups each pet follows the audio endpoint that matches its
|
|
53
|
+
own screen, falling back to the default output device.
|
|
54
|
+
|
|
55
|
+
Audio features are Windows-only and degrade to "no pulse" elsewhere.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
32
59
|
## Install
|
|
33
60
|
|
|
34
61
|
- **System Requirements**
|
|
@@ -12,6 +12,7 @@ from PySide6.QtWidgets import QMenu, QMessageBox, QWidget
|
|
|
12
12
|
from frontengine.show.base_widget import BaseWidget
|
|
13
13
|
from frontengine.show.window_helpers import apply_overlay_window_flags
|
|
14
14
|
from frontengine.user_setting.user_setting_file import user_setting_dict
|
|
15
|
+
from frontengine.utils.audio_meter.audio_envelope import AudioEnvelope
|
|
15
16
|
from frontengine.utils.logging.loggin_instance import front_engine_logger
|
|
16
17
|
from frontengine.utils.multi_language.language_wrapper import language_wrapper
|
|
17
18
|
|
|
@@ -82,6 +83,28 @@ def scan_pet_pack(folder: str) -> dict:
|
|
|
82
83
|
return result
|
|
83
84
|
|
|
84
85
|
|
|
86
|
+
# 拖進寵物的檔案分類 / What a file dropped onto the pet means
|
|
87
|
+
DROP_SPRITE = "sprite" # 圖片或動作包 -> 換一套外觀
|
|
88
|
+
DROP_FOOD = "food" # 其他檔案 -> 當成食物餵給寵物
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def classify_drop(path) -> Optional[str]:
|
|
92
|
+
"""
|
|
93
|
+
判斷拖到寵物身上的檔案要換裝還是當食物;路徑無效或空資料夾回傳 None。
|
|
94
|
+
Classify a file dropped onto the pet: a sprite/pack to wear, food to eat,
|
|
95
|
+
or None when the path is unusable.
|
|
96
|
+
"""
|
|
97
|
+
try:
|
|
98
|
+
target = Path(path)
|
|
99
|
+
if not target.exists():
|
|
100
|
+
return None
|
|
101
|
+
except (OSError, TypeError, ValueError):
|
|
102
|
+
return None
|
|
103
|
+
if target.is_dir():
|
|
104
|
+
return DROP_SPRITE if scan_pet_pack(str(target)) else None
|
|
105
|
+
return DROP_SPRITE if target.suffix.lower() in _PACK_EXTS else DROP_FOOD
|
|
106
|
+
|
|
107
|
+
|
|
85
108
|
def derive_visual_state(dragging: bool, airborne: bool, surface: str, motion_state: str) -> str:
|
|
86
109
|
"""依目前情形推導應顯示的視覺狀態 / Derive which sprite state to show."""
|
|
87
110
|
if dragging:
|
|
@@ -876,6 +899,7 @@ class DesktopPetWidget(BaseWidget):
|
|
|
876
899
|
message_box.show()
|
|
877
900
|
|
|
878
901
|
self.resize(self.pet_size, self.pet_size)
|
|
902
|
+
self.setAcceptDrops(True) # drop an image to dress it up, anything else to feed it
|
|
879
903
|
self.motion = PetMotion(
|
|
880
904
|
0, 0, self.pet_size, self.pet_size, (0, 0, 1920, 1080), speed, behaviour, climb=climb
|
|
881
905
|
)
|
|
@@ -901,6 +925,9 @@ class DesktopPetWidget(BaseWidget):
|
|
|
901
925
|
self._audio_react = bool(audio_react)
|
|
902
926
|
self._audio_level_provider = None # callable -> level 0..1 or None
|
|
903
927
|
self._audio_scale = 1.0
|
|
928
|
+
# 峰值抖動大,先過 RMS + 快起慢降包絡再驅動脈動
|
|
929
|
+
# Raw peaks jitter, so smooth them through an RMS + attack/decay envelope.
|
|
930
|
+
self._audio_envelope = AudioEnvelope()
|
|
904
931
|
# 多寵物互動 / Multi-pet interaction
|
|
905
932
|
self._peers_provider = None
|
|
906
933
|
self._peer_greeted = False
|
|
@@ -992,19 +1019,50 @@ class DesktopPetWidget(BaseWidget):
|
|
|
992
1019
|
self._peer_greeted = False
|
|
993
1020
|
|
|
994
1021
|
def _load_sprite(self, path: str):
|
|
1022
|
+
"""載入一張精靈;檔案損毀或格式不支援時回傳 None。"""
|
|
995
1023
|
if Path(path).suffix.lower() in (".gif", ".webp"):
|
|
996
1024
|
movie = QMovie(str(path))
|
|
1025
|
+
if not movie.isValid():
|
|
1026
|
+
return None
|
|
997
1027
|
movie.frameChanged.connect(self.update)
|
|
998
1028
|
return ("movie", movie)
|
|
999
|
-
|
|
1029
|
+
pixmap = QPixmap(str(path))
|
|
1030
|
+
return None if pixmap.isNull() else ("pixmap", pixmap)
|
|
1000
1031
|
|
|
1001
1032
|
def _load_sprites(self, image_path: Path) -> None:
|
|
1002
|
-
"""單一檔 -> 所有狀態共用;資料夾 ->
|
|
1033
|
+
"""單一檔 -> 所有狀態共用;資料夾 -> 依檔名對應各狀態;載不起來的略過。"""
|
|
1003
1034
|
if image_path.is_dir():
|
|
1004
1035
|
for state, path in scan_pet_pack(str(image_path)).items():
|
|
1005
|
-
|
|
1036
|
+
sprite = self._load_sprite(path)
|
|
1037
|
+
if sprite is not None:
|
|
1038
|
+
self._sprites[state] = sprite
|
|
1006
1039
|
elif image_path.is_file():
|
|
1007
|
-
|
|
1040
|
+
sprite = self._load_sprite(str(image_path))
|
|
1041
|
+
if sprite is not None:
|
|
1042
|
+
self._sprites["default"] = sprite
|
|
1043
|
+
|
|
1044
|
+
def change_sprite(self, path) -> None:
|
|
1045
|
+
"""換一套外觀(拖圖片或動作包到寵物身上時觸發);載不到就維持原樣。"""
|
|
1046
|
+
image_path = Path(path)
|
|
1047
|
+
previous = self._sprites
|
|
1048
|
+
self._sprites = {}
|
|
1049
|
+
self._load_sprites(image_path)
|
|
1050
|
+
if not self._sprites:
|
|
1051
|
+
front_engine_logger.warning(f"[DesktopPetWidget] change_sprite ignored: {image_path}")
|
|
1052
|
+
self._sprites = previous
|
|
1053
|
+
return
|
|
1054
|
+
front_engine_logger.info(f"[DesktopPetWidget] change_sprite | path={image_path}")
|
|
1055
|
+
for kind, obj in previous.values():
|
|
1056
|
+
if kind == "movie":
|
|
1057
|
+
obj.stop()
|
|
1058
|
+
self.image_path = image_path
|
|
1059
|
+
self._active = None
|
|
1060
|
+
self._active_state = None
|
|
1061
|
+
self._set_active_sprite(self._visual_state())
|
|
1062
|
+
self._gain_affection(2)
|
|
1063
|
+
pool = self._messages.get("costume") or []
|
|
1064
|
+
if pool:
|
|
1065
|
+
self.say(pool[self._chatter_rng.randrange(len(pool))])
|
|
1008
1066
|
|
|
1009
1067
|
def _sprite_for(self, state: str):
|
|
1010
1068
|
if "default" in self._sprites:
|
|
@@ -1042,16 +1100,23 @@ class DesktopPetWidget(BaseWidget):
|
|
|
1042
1100
|
def set_audio_level_provider(self, provider) -> None:
|
|
1043
1101
|
"""設定回傳目前音量 (0~1 或 None) 的函式 / Provider of the current audio level."""
|
|
1044
1102
|
self._audio_level_provider = provider
|
|
1103
|
+
self._audio_envelope.reset()
|
|
1045
1104
|
|
|
1046
1105
|
def _update_audio_scale(self) -> None:
|
|
1106
|
+
"""取一次音量取樣,經包絡平滑後換算成脈動比例(無音源則不脈動)。"""
|
|
1047
1107
|
if not self._audio_react or self._audio_level_provider is None:
|
|
1048
1108
|
self._audio_scale = 1.0
|
|
1109
|
+
self._audio_envelope.reset()
|
|
1049
1110
|
return
|
|
1050
1111
|
try:
|
|
1051
1112
|
level = self._audio_level_provider()
|
|
1052
1113
|
except Exception: # pragma: no cover - defensive
|
|
1053
1114
|
level = None
|
|
1054
|
-
|
|
1115
|
+
if level is None:
|
|
1116
|
+
self._audio_scale = 1.0
|
|
1117
|
+
self._audio_envelope.reset()
|
|
1118
|
+
return
|
|
1119
|
+
self._audio_scale = audio_pulse_scale(self._audio_envelope.push(level))
|
|
1055
1120
|
|
|
1056
1121
|
def draw_content(self, painter: QPainter) -> None:
|
|
1057
1122
|
pixmap = self._current_pixmap()
|
|
@@ -1101,6 +1166,7 @@ class DesktopPetWidget(BaseWidget):
|
|
|
1101
1166
|
"fed": lines("pet_chatter_fed", "Yum!|Thank you!|Delicious!"),
|
|
1102
1167
|
"away": lines("pet_chatter_away", "Still there?|I'll nap till you're back~|Zzz..."),
|
|
1103
1168
|
"levelup": lines("pet_chatter_levelup", "Level up!|I'm growing!|We're getting closer!"),
|
|
1169
|
+
"costume": lines("pet_chatter_costume", "New look!|How do I look?|Nice fit!"),
|
|
1104
1170
|
"any": lines("pet_chatter_any", "Hi there!|(^_^)|Keep going!"),
|
|
1105
1171
|
}
|
|
1106
1172
|
|
|
@@ -1389,6 +1455,38 @@ class DesktopPetWidget(BaseWidget):
|
|
|
1389
1455
|
self.say() # a click (no drag) makes the pet talk
|
|
1390
1456
|
super().mouseReleaseEvent(event)
|
|
1391
1457
|
|
|
1458
|
+
# --- drop a file onto the pet: wear it (image / pack) or eat it ---
|
|
1459
|
+
@staticmethod
|
|
1460
|
+
def dropped_path(mime_data) -> Optional[str]:
|
|
1461
|
+
"""從拖曳資料取出第一個可用的本機路徑;沒有可用者回傳 None。"""
|
|
1462
|
+
if mime_data is None or not mime_data.hasUrls():
|
|
1463
|
+
return None
|
|
1464
|
+
for url in mime_data.urls():
|
|
1465
|
+
local_path = url.toLocalFile()
|
|
1466
|
+
if local_path and classify_drop(local_path) is not None:
|
|
1467
|
+
return local_path
|
|
1468
|
+
return None
|
|
1469
|
+
|
|
1470
|
+
def dragEnterEvent(self, event) -> None:
|
|
1471
|
+
if self.dropped_path(event.mimeData()) is not None:
|
|
1472
|
+
event.acceptProposedAction()
|
|
1473
|
+
|
|
1474
|
+
def dragMoveEvent(self, event) -> None:
|
|
1475
|
+
if self.dropped_path(event.mimeData()) is not None:
|
|
1476
|
+
event.acceptProposedAction()
|
|
1477
|
+
|
|
1478
|
+
def dropEvent(self, event) -> None:
|
|
1479
|
+
path = self.dropped_path(event.mimeData())
|
|
1480
|
+
if path is None:
|
|
1481
|
+
return
|
|
1482
|
+
front_engine_logger.info(f"[DesktopPetWidget] dropEvent | path={path}")
|
|
1483
|
+
self.motion.wake()
|
|
1484
|
+
if classify_drop(path) == DROP_SPRITE:
|
|
1485
|
+
self.change_sprite(path)
|
|
1486
|
+
else:
|
|
1487
|
+
self.feed()
|
|
1488
|
+
event.acceptProposedAction()
|
|
1489
|
+
|
|
1392
1490
|
def contextMenuEvent(self, event) -> None:
|
|
1393
1491
|
self.menu.popup(event.globalPos())
|
|
1394
1492
|
|
|
@@ -17,7 +17,7 @@ from frontengine.ui.page.utils import (
|
|
|
17
17
|
resolve_preferred_monitor,
|
|
18
18
|
)
|
|
19
19
|
from frontengine.user_setting.user_setting_file import add_recent_file
|
|
20
|
-
from frontengine.utils.audio_meter.
|
|
20
|
+
from frontengine.utils.audio_meter.screen_audio import audio_level_provider_for_screen
|
|
21
21
|
from frontengine.utils.logging.loggin_instance import front_engine_logger
|
|
22
22
|
from frontengine.utils.multi_language.language_wrapper import language_wrapper
|
|
23
23
|
|
|
@@ -93,6 +93,12 @@ class PetSettingUI(QWidget):
|
|
|
93
93
|
self.volume_combobox.setCurrentText("100%")
|
|
94
94
|
self.audio_react_checkbox = QCheckBox(
|
|
95
95
|
language_wrapper.language_word_dict.get("pet_audio_label", "React to audio"))
|
|
96
|
+
self.drop_hint_label = QLabel(
|
|
97
|
+
language_wrapper.language_word_dict.get(
|
|
98
|
+
"pet_drop_hint",
|
|
99
|
+
"Tip: drop an image or pet pack onto the pet to change its look, "
|
|
100
|
+
"or any other file to feed it."))
|
|
101
|
+
self.drop_hint_label.setWordWrap(True)
|
|
96
102
|
|
|
97
103
|
# Start
|
|
98
104
|
self.start_button = QPushButton(language_wrapper.language_word_dict.get("pet_start", "Spawn pet"))
|
|
@@ -128,6 +134,7 @@ class PetSettingUI(QWidget):
|
|
|
128
134
|
self.grid_layout.addWidget(self.volume_label, 6, 2)
|
|
129
135
|
self.grid_layout.addWidget(self.volume_combobox, 7, 2)
|
|
130
136
|
self.grid_layout.addWidget(self.audio_react_checkbox, 7, 0)
|
|
137
|
+
self.grid_layout.addWidget(self.drop_hint_label, 8, 0, 1, 3)
|
|
131
138
|
|
|
132
139
|
def _spawn_pet(self) -> None:
|
|
133
140
|
"""建立、顯示並開始移動一隻寵物(供 Start 與右鍵複製共用)。"""
|
|
@@ -147,12 +154,15 @@ class PetSettingUI(QWidget):
|
|
|
147
154
|
)
|
|
148
155
|
pet.clone_requested.connect(self._spawn_pet)
|
|
149
156
|
pet.set_peers_provider(lambda me=pet: self._peer_centers(me))
|
|
157
|
+
geometry = self._target_geometry()
|
|
150
158
|
if self.audio_react_checkbox.isChecked():
|
|
151
|
-
|
|
159
|
+
# 跟隨寵物所在螢幕的音源(比對不到就用系統預設輸出裝置)
|
|
160
|
+
# Follow the audio of the screen the pet lives on; default endpoint otherwise.
|
|
161
|
+
pet.set_audio_level_provider(
|
|
162
|
+
audio_level_provider_for_screen(geometry[1] if geometry is not None else None))
|
|
152
163
|
pet.set_pet_window_flag()
|
|
153
164
|
self.pet_list.append(pet)
|
|
154
165
|
pet.show()
|
|
155
|
-
geometry = self._target_geometry()
|
|
156
166
|
if geometry is not None:
|
|
157
167
|
pet.setScreen(geometry[1])
|
|
158
168
|
bounds = geometry[0]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
把逐次取樣的音量峰值平滑成自然的脈動包絡:先在短窗內取 RMS 壓掉抖動,
|
|
3
|
+
再以「快起慢降」的包絡追蹤器塑形,避免忽大忽小的閃爍。純邏輯、不依賴 Qt。
|
|
4
|
+
|
|
5
|
+
Smooth per-tick peak samples into a natural pulse envelope: a short RMS window
|
|
6
|
+
removes jitter, then an asymmetric follower (fast attack, slow decay) shapes it.
|
|
7
|
+
Pure logic, no Qt dependency.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections import deque
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def clamp_level(value) -> float:
|
|
15
|
+
"""把任意輸入夾到 0~1;無法轉成數值時回傳 0.0。"""
|
|
16
|
+
try:
|
|
17
|
+
return max(0.0, min(1.0, float(value)))
|
|
18
|
+
except (TypeError, ValueError):
|
|
19
|
+
return 0.0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AudioEnvelope:
|
|
23
|
+
"""
|
|
24
|
+
音量包絡追蹤器:push() 餵進一個 0~1 的峰值取樣,回傳平滑後的 0~1 音量。
|
|
25
|
+
window 為 RMS 取樣窗長度,attack/decay 為上升/下降的追隨係數 (0~1,越大越快)。
|
|
26
|
+
|
|
27
|
+
An envelope follower: push() takes one 0..1 peak sample and returns the
|
|
28
|
+
smoothed 0..1 level. `window` is the RMS window length; `attack`/`decay`
|
|
29
|
+
are the rise/fall follow coefficients (0..1, larger is faster).
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, window: int = 4, attack: float = 0.6, decay: float = 0.12) -> None:
|
|
33
|
+
self._window = max(1, int(window))
|
|
34
|
+
self._samples: deque = deque(maxlen=self._window)
|
|
35
|
+
self.attack = clamp_level(attack)
|
|
36
|
+
self.decay = clamp_level(decay)
|
|
37
|
+
self.value = 0.0
|
|
38
|
+
|
|
39
|
+
def reset(self) -> None:
|
|
40
|
+
"""清空取樣與輸出(音源消失或關閉律動時呼叫)。"""
|
|
41
|
+
self._samples.clear()
|
|
42
|
+
self.value = 0.0
|
|
43
|
+
|
|
44
|
+
def rms(self) -> float:
|
|
45
|
+
"""目前取樣窗的均方根;窗內無取樣時回傳 0.0。"""
|
|
46
|
+
if not self._samples:
|
|
47
|
+
return 0.0
|
|
48
|
+
return (sum(sample * sample for sample in self._samples) / len(self._samples)) ** 0.5
|
|
49
|
+
|
|
50
|
+
def push(self, level) -> float:
|
|
51
|
+
"""餵入一個峰值取樣並回傳平滑後的音量 / Feed one peak sample, return the smoothed level."""
|
|
52
|
+
self._samples.append(clamp_level(level))
|
|
53
|
+
target = self.rms()
|
|
54
|
+
coefficient = self.attack if target > self.value else self.decay
|
|
55
|
+
self.value = clamp_level(self.value + coefficient * (target - self.value))
|
|
56
|
+
return self.value
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""
|
|
2
|
+
把「螢幕」對應到「輸出音源端點」:多螢幕時,各螢幕常各自帶喇叭(HDMI/DP 音訊),
|
|
3
|
+
本模組以端點顯示名稱與螢幕型號/製造商的共同字詞比對,讓每隻寵物跟著自己所在
|
|
4
|
+
螢幕的聲音律動;比不到對應端點時退回預設輸出裝置。
|
|
5
|
+
|
|
6
|
+
Map a screen to an audio output endpoint. On multi-monitor setups each screen
|
|
7
|
+
often carries its own speakers (HDMI/DP audio); this matches endpoint friendly
|
|
8
|
+
names against the screen's model/manufacturer so a pet pulses to the sound of
|
|
9
|
+
the screen it lives on, falling back to the default endpoint when unmatched.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Dict, Iterable, List, Optional, Tuple
|
|
14
|
+
|
|
15
|
+
from frontengine.utils.audio_meter.system_audio_meter import (
|
|
16
|
+
SystemAudioMeter, list_output_devices, system_audio_level,
|
|
17
|
+
)
|
|
18
|
+
from frontengine.utils.logging.loggin_instance import front_engine_logger
|
|
19
|
+
|
|
20
|
+
# 比對時要忽略的通用字詞(裝置與螢幕名稱都常出現,沒有辨識力)
|
|
21
|
+
# Generic words that carry no identifying power when matching names.
|
|
22
|
+
_GENERIC_TOKENS = frozenset({
|
|
23
|
+
"audio", "sound", "speaker", "speakers", "headphone", "headphones", "headset",
|
|
24
|
+
"digital", "output", "high", "definition", "device", "generic", "pnp", "plug",
|
|
25
|
+
"play", "monitor", "display", "screen", "hdmi", "displayport", "the", "and",
|
|
26
|
+
"nvidia", "intel", "amd", "realtek", "usb", "default", "communications",
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def name_tokens(text) -> set:
|
|
31
|
+
"""把名稱切成可比對的小寫字詞,去掉通用字詞與過短片段。"""
|
|
32
|
+
if not text:
|
|
33
|
+
return set()
|
|
34
|
+
tokens = set()
|
|
35
|
+
word = []
|
|
36
|
+
for character in str(text).lower():
|
|
37
|
+
if character.isalnum():
|
|
38
|
+
word.append(character)
|
|
39
|
+
continue
|
|
40
|
+
if word:
|
|
41
|
+
tokens.add("".join(word))
|
|
42
|
+
word = []
|
|
43
|
+
if word:
|
|
44
|
+
tokens.add("".join(word))
|
|
45
|
+
return {token for token in tokens if len(token) >= 2 and token not in _GENERIC_TOKENS}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def match_device_for_screen(hints: Iterable[str], devices: Iterable[Tuple[str, str]]) -> Optional[str]:
|
|
49
|
+
"""
|
|
50
|
+
以共同字詞數挑出最像該螢幕的輸出端點,回傳 device_id;沒有共同字詞回傳 None。
|
|
51
|
+
Pick the endpoint sharing the most tokens with the screen hints, or None.
|
|
52
|
+
"""
|
|
53
|
+
hint_tokens = set()
|
|
54
|
+
for hint in hints or ():
|
|
55
|
+
hint_tokens |= name_tokens(hint)
|
|
56
|
+
if not hint_tokens:
|
|
57
|
+
return None
|
|
58
|
+
best_id = None
|
|
59
|
+
best_score = 0
|
|
60
|
+
for device_id, name in devices or ():
|
|
61
|
+
score = len(hint_tokens & name_tokens(name))
|
|
62
|
+
if score > best_score:
|
|
63
|
+
best_score = score
|
|
64
|
+
best_id = device_id
|
|
65
|
+
return best_id
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def screen_hints(screen) -> List[str]:
|
|
69
|
+
"""從 QScreen 取出可比對的字串(型號、製造商、名稱),取不到就略過。"""
|
|
70
|
+
hints: List[str] = []
|
|
71
|
+
for attribute in ("model", "manufacturer", "name"):
|
|
72
|
+
getter = getattr(screen, attribute, None)
|
|
73
|
+
if not callable(getter):
|
|
74
|
+
continue
|
|
75
|
+
try:
|
|
76
|
+
value = getter()
|
|
77
|
+
except Exception as error: # pragma: no cover - defensive against Qt/duck-typed screens
|
|
78
|
+
# 某個欄位問不到不影響其他欄位的比對,記錄後繼續。
|
|
79
|
+
# One attribute refusing to answer must not stop the remaining hints.
|
|
80
|
+
front_engine_logger.debug(f"[screen_hints] {attribute} failed: {error!r}")
|
|
81
|
+
continue
|
|
82
|
+
if value:
|
|
83
|
+
hints.append(str(value))
|
|
84
|
+
return hints
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ScreenAudioMeters:
|
|
88
|
+
"""依裝置 ID 快取電表,並提供「綁定某螢幕音源」的取樣函式。"""
|
|
89
|
+
|
|
90
|
+
def __init__(self) -> None:
|
|
91
|
+
self._meters: Dict[str, SystemAudioMeter] = {}
|
|
92
|
+
|
|
93
|
+
def device_for_screen(self, screen) -> Optional[str]:
|
|
94
|
+
"""回傳該螢幕對應的輸出端點 ID;沒有對應者回傳 None。"""
|
|
95
|
+
if screen is None:
|
|
96
|
+
return None
|
|
97
|
+
return match_device_for_screen(screen_hints(screen), list_output_devices())
|
|
98
|
+
|
|
99
|
+
def level_for_device(self, device_id: str) -> Optional[float]:
|
|
100
|
+
"""讀取指定端點的目前峰值(電表物件會重複使用)。"""
|
|
101
|
+
meter = self._meters.get(device_id)
|
|
102
|
+
if meter is None:
|
|
103
|
+
meter = SystemAudioMeter(device_id)
|
|
104
|
+
self._meters[device_id] = meter
|
|
105
|
+
return meter.level()
|
|
106
|
+
|
|
107
|
+
def provider_for_screen(self, screen):
|
|
108
|
+
"""
|
|
109
|
+
回傳取樣函式:優先跟隨該螢幕的音源,比對不到則沿用系統預設輸出裝置。
|
|
110
|
+
Return a level provider bound to the screen's endpoint, or the default one.
|
|
111
|
+
"""
|
|
112
|
+
device_id = self.device_for_screen(screen)
|
|
113
|
+
if device_id is None:
|
|
114
|
+
return system_audio_level
|
|
115
|
+
front_engine_logger.info(f"[ScreenAudioMeters] screen audio endpoint | id={device_id}")
|
|
116
|
+
return lambda: self.level_for_device(device_id)
|
|
117
|
+
|
|
118
|
+
def close(self) -> None:
|
|
119
|
+
"""釋放所有已建立的電表 / Release every meter created so far."""
|
|
120
|
+
for meter in self._meters.values():
|
|
121
|
+
meter.close()
|
|
122
|
+
self._meters.clear()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
_screen_meters = ScreenAudioMeters()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def audio_level_provider_for_screen(screen):
|
|
129
|
+
"""便利函式:取得跟隨指定螢幕音源的取樣函式(共用電表快取)。"""
|
|
130
|
+
return _screen_meters.provider_for_screen(screen)
|