frontengine 1.0.4__tar.gz → 1.0.6__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 (136) hide show
  1. {frontengine-1.0.4 → frontengine-1.0.6}/PKG-INFO +1 -1
  2. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/show/pet/desktop_pet.py +246 -24
  3. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/pet/pet_setting_ui.py +36 -2
  4. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/multi_language/english.py +8 -0
  5. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/multi_language/traditional_chinese.py +8 -0
  6. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine.egg-info/PKG-INFO +1 -1
  7. {frontengine-1.0.4 → frontengine-1.0.6}/pyproject.toml +1 -1
  8. {frontengine-1.0.4 → frontengine-1.0.6}/LICENSE +0 -0
  9. {frontengine-1.0.4 → frontengine-1.0.6}/README.md +0 -0
  10. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/__init__.py +0 -0
  11. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/__main__.py +0 -0
  12. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/show/__init__.py +0 -0
  13. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/show/base_widget.py +0 -0
  14. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/show/gif/__init__.py +0 -0
  15. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/show/gif/paint_gif.py +0 -0
  16. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/show/image/__init__.py +0 -0
  17. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/show/image/paint_image.py +0 -0
  18. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/show/load/__init__.py +0 -0
  19. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/show/load/load_someone_make_ui.py +0 -0
  20. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/show/overlay_factory.py +0 -0
  21. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/show/particle/__init__.py +0 -0
  22. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/show/particle/particle_ui.py +0 -0
  23. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/show/pet/__init__.py +0 -0
  24. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/show/scene/__init__.py +0 -0
  25. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/show/scene/extend_graphic_scene.py +0 -0
  26. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/show/scene/extend_graphic_view.py +0 -0
  27. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/show/scene/scene.py +0 -0
  28. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/show/sound_player/__init__.py +0 -0
  29. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/show/sound_player/sound_effect.py +0 -0
  30. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/show/sound_player/sound_player.py +0 -0
  31. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/show/text/__init__.py +0 -0
  32. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/show/text/draw_text.py +0 -0
  33. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/show/video/__init__.py +0 -0
  34. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/show/video/video_player.py +0 -0
  35. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/show/web/__init__.py +0 -0
  36. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/show/web/webview.py +0 -0
  37. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/show/window_helpers.py +0 -0
  38. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/system_tray/__init__.py +0 -0
  39. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/system_tray/extend_system_tray.py +0 -0
  40. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/__init__.py +0 -0
  41. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/color/__init__.py +0 -0
  42. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/color/global_color.py +0 -0
  43. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/dialog/__init__.py +0 -0
  44. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/dialog/choose_file_dialog.py +0 -0
  45. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/dialog/hotkey_settings_dialog.py +0 -0
  46. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/dialog/save_file_dialog.py +0 -0
  47. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/main_ui.py +0 -0
  48. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/menu/__init__.py +0 -0
  49. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/menu/help_menu.py +0 -0
  50. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/menu/how_to_menu.py +0 -0
  51. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/menu/language_menu.py +0 -0
  52. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/menu/preset_menu.py +0 -0
  53. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/menu/settings_menu.py +0 -0
  54. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/__init__.py +0 -0
  55. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/control_center/__init__.py +0 -0
  56. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/control_center/control_center_ui.py +0 -0
  57. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/gif/__init__.py +0 -0
  58. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/gif/gif_setting_ui.py +0 -0
  59. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/image/__init__.py +0 -0
  60. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/image/image_setting_ui.py +0 -0
  61. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/particle/__init__.py +0 -0
  62. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/particle/particle_setting_ui.py +0 -0
  63. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/pet/__init__.py +0 -0
  64. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/scene_setting/__init__.py +0 -0
  65. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/scene_setting/scene_manager.py +0 -0
  66. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/scene_setting/scene_page/__init__.py +0 -0
  67. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/scene_setting/scene_page/base_scene_page.py +0 -0
  68. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/scene_setting/scene_page/gif.py +0 -0
  69. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/scene_setting/scene_page/image.py +0 -0
  70. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/scene_setting/scene_page/registry.py +0 -0
  71. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/scene_setting/scene_page/sound.py +0 -0
  72. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/scene_setting/scene_page/text.py +0 -0
  73. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/scene_setting/scene_page/video.py +0 -0
  74. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/scene_setting/scene_page/web.py +0 -0
  75. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/scene_setting/scene_setting_ui.py +0 -0
  76. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/sound_player/__init__.py +0 -0
  77. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/sound_player/sound_player_setting_ui.py +0 -0
  78. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/text/__init__.py +0 -0
  79. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/text/text_setting_ui.py +0 -0
  80. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/utils.py +0 -0
  81. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/video/__init__.py +0 -0
  82. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/video/video_setting_ui.py +0 -0
  83. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/web/__init__.py +0 -0
  84. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/ui/page/web/web_setting_ui.py +0 -0
  85. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/user_setting/__init__.py +0 -0
  86. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/user_setting/preset_repository.py +0 -0
  87. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/user_setting/scene_setting.py +0 -0
  88. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/user_setting/user_setting_file.py +0 -0
  89. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/__init__.py +0 -0
  90. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/browser/__init__.py +0 -0
  91. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/browser/browser.py +0 -0
  92. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/critical_exit/__init__.py +0 -0
  93. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/critical_exit/check_key_is_press.py +0 -0
  94. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/critical_exit/critical_exit.py +0 -0
  95. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/critical_exit/win32_vk.py +0 -0
  96. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/exception/__init__.py +0 -0
  97. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/exception/exception_tags.py +0 -0
  98. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/exception/exceptions.py +0 -0
  99. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/file/__init__.py +0 -0
  100. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/file/open/__init__.py +0 -0
  101. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/file/open/open_file.py +0 -0
  102. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/file/save/__init__.py +0 -0
  103. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/file/save/save_file.py +0 -0
  104. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/hotkey/__init__.py +0 -0
  105. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/hotkey/hotkey_service.py +0 -0
  106. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/json/__init__.py +0 -0
  107. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/json/json_file.py +0 -0
  108. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/json/json_repository.py +0 -0
  109. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/json_format/__init__.py +0 -0
  110. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/json_format/json_process.py +0 -0
  111. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/logging/__init__.py +0 -0
  112. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/logging/loggin_instance.py +0 -0
  113. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/multi_language/__init__.py +0 -0
  114. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/multi_language/france.py +0 -0
  115. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/multi_language/germany.py +0 -0
  116. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/multi_language/italy.py +0 -0
  117. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/multi_language/language_wrapper.py +0 -0
  118. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/multi_language/russian.py +0 -0
  119. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/multi_language/simplified_chinese.py +0 -0
  120. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/preset_schedule/__init__.py +0 -0
  121. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/preset_schedule/preset_schedule_service.py +0 -0
  122. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/redirect_manager/__init__.py +0 -0
  123. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/redirect_manager/redirect_manager_class.py +0 -0
  124. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/smart_pause/__init__.py +0 -0
  125. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/smart_pause/smart_pause_service.py +0 -0
  126. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/theme_schedule/__init__.py +0 -0
  127. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/theme_schedule/theme_schedule_service.py +0 -0
  128. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/utils/web_url.py +0 -0
  129. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/worker/__init__.py +0 -0
  130. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine/worker/qthread_worker.py +0 -0
  131. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine.egg-info/SOURCES.txt +0 -0
  132. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine.egg-info/dependency_links.txt +0 -0
  133. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine.egg-info/entry_points.txt +0 -0
  134. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine.egg-info/requires.txt +0 -0
  135. {frontengine-1.0.4 → frontengine-1.0.6}/frontengine.egg-info/top_level.txt +0 -0
  136. {frontengine-1.0.4 → frontengine-1.0.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frontengine
3
- Version: 1.0.4
3
+ Version: 1.0.6
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
@@ -1,10 +1,11 @@
1
1
  import random as _random_module
2
+ from datetime import datetime
2
3
  from pathlib import Path
3
4
  from typing import Optional, Tuple
4
5
 
5
6
  from PySide6.QtCore import Qt, QRect, QTimer, Signal
6
- from PySide6.QtGui import QAction, QCursor, QMovie, QPainter, QPixmap, QTransform
7
- from PySide6.QtWidgets import QMenu, QMessageBox
7
+ from PySide6.QtGui import QAction, QColor, QCursor, QFont, QFontMetrics, QMovie, QPainter, QPixmap, QTransform
8
+ from PySide6.QtWidgets import QMenu, QMessageBox, QWidget
8
9
 
9
10
  from frontengine.show.base_widget import BaseWidget
10
11
  from frontengine.show.window_helpers import apply_overlay_window_flags
@@ -22,6 +23,131 @@ SURFACE_LEFT = "left"
22
23
  SURFACE_CEILING = "ceiling"
23
24
  SURFACE_RIGHT = "right"
24
25
 
26
+ # 視覺狀態(決定顯示哪張精靈)/ Visual states (which sprite to show)
27
+ STATE_WALK = "walk"
28
+ STATE_IDLE = "idle"
29
+ STATE_CLIMB = "climb"
30
+ STATE_FALL = "fall"
31
+ STATE_DRAG = "drag"
32
+
33
+ # 動作包(資料夾)中,各狀態可接受的檔名(不含副檔名,先命中者為準)
34
+ # Accepted file stems per state inside a pet pack folder (first match wins).
35
+ _PACK_STATE_ALIASES = {
36
+ STATE_WALK: ("walk", "run", "move"),
37
+ STATE_IDLE: ("idle", "sit", "stand", "sleep"),
38
+ STATE_CLIMB: ("climb", "grab", "wall"),
39
+ STATE_FALL: ("fall", "jump"),
40
+ STATE_DRAG: ("drag", "pinch", "grabbed", "held"),
41
+ }
42
+ _PACK_EXTS = (".gif", ".webp", ".png", ".jpg", ".jpeg")
43
+
44
+
45
+ def scan_pet_pack(folder: str) -> dict:
46
+ """
47
+ 掃描動作包資料夾,回傳「視覺狀態 -> 圖片路徑」對應。非資料夾回傳空 dict。
48
+ Scan a pet-pack folder and return a ``state -> image path`` mapping.
49
+ Returns {} when `folder` is not a directory.
50
+ """
51
+ result: dict = {}
52
+ try:
53
+ folder_path = Path(folder)
54
+ if not folder_path.is_dir():
55
+ return result
56
+ by_stem = {
57
+ path.stem.lower(): path
58
+ for path in sorted(folder_path.iterdir())
59
+ if path.is_file() and path.suffix.lower() in _PACK_EXTS
60
+ }
61
+ for state, aliases in _PACK_STATE_ALIASES.items():
62
+ for alias in aliases:
63
+ if alias in by_stem:
64
+ result[state] = str(by_stem[alias])
65
+ break
66
+ except OSError:
67
+ pass
68
+ return result
69
+
70
+
71
+ def derive_visual_state(dragging: bool, airborne: bool, surface: str, motion_state: str) -> str:
72
+ """依目前情形推導應顯示的視覺狀態 / Derive which sprite state to show."""
73
+ if dragging:
74
+ return STATE_DRAG
75
+ if airborne:
76
+ return STATE_FALL
77
+ if surface in (SURFACE_LEFT, SURFACE_RIGHT, SURFACE_CEILING):
78
+ return STATE_CLIMB
79
+ if surface == SURFACE_FLOOR and motion_state == "idle":
80
+ return STATE_IDLE
81
+ return STATE_WALK
82
+
83
+
84
+ def message_bucket(hour: int) -> str:
85
+ """依小時回傳時段 / Time-of-day bucket for the given hour."""
86
+ hour = int(hour) % 24
87
+ if 5 <= hour < 12:
88
+ return "morning"
89
+ if 12 <= hour < 18:
90
+ return "afternoon"
91
+ if 18 <= hour < 22:
92
+ return "evening"
93
+ return "night"
94
+
95
+
96
+ def pick_message(hour: int, rng, messages: dict) -> str:
97
+ """
98
+ 依時段從對應句庫(加上通用句庫)隨機挑一句台詞;句庫皆空回傳空字串。
99
+ Pick a chatter line from the time bucket plus the generic pool. rng needs
100
+ a ``randrange`` method (injectable for tests).
101
+ """
102
+ pool = list(messages.get(message_bucket(hour), [])) + list(messages.get("any", []))
103
+ if not pool:
104
+ return ""
105
+ return pool[rng.randrange(len(pool))]
106
+
107
+
108
+ class SpeechBubble(QWidget):
109
+ """寵物頭上的小對話泡泡,數秒後自動消失 / A small speech bubble that auto-hides."""
110
+
111
+ def __init__(self) -> None:
112
+ super().__init__()
113
+ self._text = ""
114
+ self._font = QFont()
115
+ self._font.setPointSize(11)
116
+ self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
117
+ apply_overlay_window_flags(self, show_on_bottom=False, allow_input=False)
118
+ self._hide_timer = QTimer(self)
119
+ self._hide_timer.setSingleShot(True)
120
+ self._hide_timer.timeout.connect(self.hide)
121
+
122
+ def show_message(self, text: str, anchor: QWidget, duration_ms: int = 4000) -> None:
123
+ self._text = text
124
+ width = min(260, QFontMetrics(self._font).horizontalAdvance(text) + 28)
125
+ self.resize(max(60, width), 44)
126
+ self.reposition(anchor)
127
+ self.show()
128
+ self.raise_()
129
+ self.update()
130
+ self._hide_timer.start(max(500, int(duration_ms)))
131
+
132
+ def reposition(self, anchor: QWidget) -> None:
133
+ geo = anchor.frameGeometry()
134
+ self.move(geo.center().x() - self.width() // 2, geo.top() - self.height() - 6)
135
+
136
+ def paintEvent(self, event) -> None:
137
+ painter = QPainter(self)
138
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
139
+ rect = self.rect().adjusted(1, 1, -1, -1)
140
+ painter.setBrush(QColor(255, 255, 240, 235))
141
+ painter.setPen(QColor(90, 90, 90))
142
+ painter.drawRoundedRect(rect, 8, 8)
143
+ painter.setPen(QColor(30, 30, 30))
144
+ painter.setFont(self._font)
145
+ painter.drawText(
146
+ rect.adjusted(8, 2, -8, -2),
147
+ int(Qt.AlignmentFlag.AlignCenter | Qt.TextFlag.TextWordWrap),
148
+ self._text,
149
+ )
150
+
25
151
 
26
152
  class PetMotion:
27
153
  """
@@ -260,30 +386,26 @@ class DesktopPetWidget(BaseWidget):
260
386
  clone_requested = Signal()
261
387
 
262
388
  def __init__(self, image_path: str, size: int = 128, speed: int = 3,
263
- behaviour: str = BEHAVIOUR_FLOOR, climb: bool = True):
389
+ behaviour: str = BEHAVIOUR_FLOOR, climb: bool = True, talk: bool = True):
264
390
  front_engine_logger.info(
265
391
  f"[DesktopPetWidget] Init | path={image_path}, size={size}, speed={speed}, "
266
- f"behaviour={behaviour}, climb={climb}"
392
+ f"behaviour={behaviour}, climb={climb}, talk={talk}"
267
393
  )
268
394
  super().__init__()
269
395
  self.opacity = 1.0
270
396
  self.pet_size: int = max(16, int(size))
271
397
  self.image_path: Path = Path(image_path)
272
- self.movie: Optional[QMovie] = None
273
- self.pixmap: Optional[QPixmap] = None
274
398
  self._dragging: bool = False
275
399
  self._drag_offset = None
276
400
  self._last_move_delta: Tuple[int, int] = (0, 0)
277
-
278
- if self.image_path.exists() and self.image_path.is_file():
279
- if self.image_path.suffix.lower() in (".gif", ".webp"):
280
- self.movie = QMovie(str(self.image_path))
281
- self.movie.frameChanged.connect(self.update)
282
- self.movie.start()
283
- else:
284
- self.pixmap = QPixmap(str(self.image_path))
285
- else:
286
- front_engine_logger.error(f"[DesktopPetWidget] File not found: {self.image_path}")
401
+ # 視覺狀態 -> ("movie"|"pixmap", 物件) / state -> sprite
402
+ self._sprites: dict = {}
403
+ self._active = None
404
+ self._active_state = None
405
+
406
+ self._load_sprites(self.image_path)
407
+ if not self._sprites:
408
+ front_engine_logger.error(f"[DesktopPetWidget] No sprite found: {self.image_path}")
287
409
  message_box = QMessageBox(self)
288
410
  message_box.setText(language_wrapper.language_word_dict.get("pet_message_box_text", "Pet image not found"))
289
411
  message_box.show()
@@ -292,10 +414,21 @@ class DesktopPetWidget(BaseWidget):
292
414
  self.motion = PetMotion(
293
415
  0, 0, self.pet_size, self.pet_size, (0, 0, 1920, 1080), speed, behaviour, climb=climb
294
416
  )
417
+ self._set_active_sprite(STATE_WALK)
295
418
 
296
419
  self._timer = QTimer(self)
297
420
  self._timer.timeout.connect(self._on_tick)
298
421
 
422
+ # Speech bubbles / chatter
423
+ self._talk = bool(talk)
424
+ self._moved = False
425
+ self._chatter_rng = _random_module.SystemRandom()
426
+ self._messages = self._load_messages()
427
+ self._bubble = SpeechBubble()
428
+ self._chatter_timer = QTimer(self)
429
+ self._chatter_timer.setSingleShot(True)
430
+ self._chatter_timer.timeout.connect(self._on_chatter)
431
+
299
432
  self.menu = QMenu(self)
300
433
  self.clone_action = QAction(language_wrapper.language_word_dict.get("pet_clone", "Clone"), self)
301
434
  self.clone_action.triggered.connect(self.clone_requested.emit)
@@ -308,10 +441,53 @@ class DesktopPetWidget(BaseWidget):
308
441
  """寵物需接收滑鼠以便拖曳,故 allow_input=True 並置頂。"""
309
442
  apply_overlay_window_flags(self, show_on_bottom=False, allow_input=True)
310
443
 
444
+ def _load_sprite(self, path: str):
445
+ if Path(path).suffix.lower() in (".gif", ".webp"):
446
+ movie = QMovie(str(path))
447
+ movie.frameChanged.connect(self.update)
448
+ return ("movie", movie)
449
+ return ("pixmap", QPixmap(str(path)))
450
+
451
+ def _load_sprites(self, image_path: Path) -> None:
452
+ """單一檔 -> 所有狀態共用;資料夾 -> 依檔名對應各狀態。"""
453
+ if image_path.is_dir():
454
+ for state, path in scan_pet_pack(str(image_path)).items():
455
+ self._sprites[state] = self._load_sprite(path)
456
+ elif image_path.is_file():
457
+ self._sprites["default"] = self._load_sprite(str(image_path))
458
+
459
+ def _sprite_for(self, state: str):
460
+ if "default" in self._sprites:
461
+ return self._sprites["default"]
462
+ if state in self._sprites:
463
+ return self._sprites[state]
464
+ for fallback in (STATE_WALK, STATE_IDLE):
465
+ if fallback in self._sprites:
466
+ return self._sprites[fallback]
467
+ return next(iter(self._sprites.values()), None)
468
+
469
+ def _set_active_sprite(self, state: str) -> None:
470
+ sprite = self._sprite_for(state)
471
+ if sprite is None or sprite is self._active:
472
+ return
473
+ if self._active is not None and self._active[0] == "movie":
474
+ self._active[1].stop()
475
+ self._active = sprite
476
+ self._active_state = state
477
+ if sprite[0] == "movie":
478
+ sprite[1].start()
479
+ self.update()
480
+
481
+ def _visual_state(self) -> str:
482
+ return derive_visual_state(
483
+ self._dragging, self.motion._airborne, self.motion.surface, self.motion.state
484
+ )
485
+
311
486
  def _current_pixmap(self) -> Optional[QPixmap]:
312
- if self.movie is not None:
313
- return self.movie.currentPixmap()
314
- return self.pixmap
487
+ if self._active is None:
488
+ return None
489
+ kind, obj = self._active
490
+ return obj.currentPixmap() if kind == "movie" else obj
315
491
 
316
492
  def draw_content(self, painter: QPainter) -> None:
317
493
  pixmap = self._current_pixmap()
@@ -329,28 +505,67 @@ class DesktopPetWidget(BaseWidget):
329
505
  self.motion.y = float(bottom - self.pet_size)
330
506
  self.move(int(self.motion.x), int(self.motion.y))
331
507
  self._timer.start(max(10, int(interval_ms)))
508
+ self._schedule_chatter()
509
+
510
+ def _load_messages(self) -> dict:
511
+ d = language_wrapper.language_word_dict
512
+
513
+ def lines(key: str, fallback: str) -> list:
514
+ return [part.strip() for part in str(d.get(key, fallback)).split("|") if part.strip()]
515
+
516
+ return {
517
+ "morning": lines("pet_chatter_morning", "Good morning!|Ready for the day?"),
518
+ "afternoon": lines("pet_chatter_afternoon", "Good afternoon!|Taking a break?"),
519
+ "evening": lines("pet_chatter_evening", "Good evening!|How was your day?"),
520
+ "night": lines("pet_chatter_night", "It's late - get some rest!|Still up?"),
521
+ "any": lines("pet_chatter_any", "Hi there!|(^_^)|Keep going!"),
522
+ }
523
+
524
+ def _schedule_chatter(self) -> None:
525
+ if self._talk:
526
+ self._chatter_timer.start(self._chatter_rng.randint(15000, 35000))
527
+
528
+ def _on_chatter(self) -> None:
529
+ self.say()
530
+ self._schedule_chatter()
531
+
532
+ def say(self, text: Optional[str] = None) -> None:
533
+ """顯示一句台詞(text 為 None 時依時段隨機挑)。"""
534
+ if not self._talk:
535
+ return
536
+ if text is None:
537
+ text = pick_message(datetime.now().hour, self._chatter_rng, self._messages)
538
+ if text:
539
+ self._bubble.show_message(text, self)
332
540
 
333
541
  def _on_tick(self) -> None:
334
542
  if self._dragging:
543
+ self._set_active_sprite(STATE_DRAG)
335
544
  return
336
545
  if self.motion.behaviour == BEHAVIOUR_CHASE:
337
546
  cursor = QCursor.pos()
338
547
  self.motion.set_target(cursor.x(), cursor.y())
339
548
  x, y = self.motion.step()
340
549
  self.move(x, y)
550
+ self._set_active_sprite(self._visual_state())
551
+ if self._bubble.isVisible():
552
+ self._bubble.reposition(self)
341
553
 
342
554
  # --- dragging + throwing ---
343
555
  def mousePressEvent(self, event) -> None:
344
556
  if event.button() == Qt.MouseButton.LeftButton:
345
557
  self._dragging = True
558
+ self._moved = False
346
559
  self._last_move_delta = (0, 0)
347
560
  self._drag_offset = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
561
+ self._set_active_sprite(STATE_DRAG)
348
562
  super().mousePressEvent(event)
349
563
 
350
564
  def mouseMoveEvent(self, event) -> None:
351
565
  if self._dragging and self._drag_offset is not None:
352
566
  new_top_left = event.globalPosition().toPoint() - self._drag_offset
353
567
  self._last_move_delta = (new_top_left.x() - self.x(), new_top_left.y() - self.y())
568
+ self._moved = True
354
569
  self.move(new_top_left)
355
570
  super().mouseMoveEvent(event)
356
571
 
@@ -359,9 +574,12 @@ class DesktopPetWidget(BaseWidget):
359
574
  self._dragging = False
360
575
  self.motion.x = float(self.x())
361
576
  self.motion.y = float(self.y())
362
- # Fling with the momentum of the last drag movement (floor mode).
363
- if self.motion.behaviour == BEHAVIOUR_FLOOR:
364
- self.motion.throw(self._last_move_delta[0], self._last_move_delta[1])
577
+ if self._moved:
578
+ # Fling with the momentum of the last drag movement (floor mode).
579
+ if self.motion.behaviour == BEHAVIOUR_FLOOR:
580
+ self.motion.throw(self._last_move_delta[0], self._last_move_delta[1])
581
+ else:
582
+ self.say() # a click (no drag) makes the pet talk
365
583
  super().mouseReleaseEvent(event)
366
584
 
367
585
  def contextMenuEvent(self, event) -> None:
@@ -370,6 +588,10 @@ class DesktopPetWidget(BaseWidget):
370
588
  def closeEvent(self, event) -> None:
371
589
  if self._timer.isActive():
372
590
  self._timer.stop()
373
- if self.movie is not None:
374
- self.movie.stop()
591
+ if self._chatter_timer.isActive():
592
+ self._chatter_timer.stop()
593
+ self._bubble.close()
594
+ for kind, obj in self._sprites.values():
595
+ if kind == "movie":
596
+ obj.stop()
375
597
  super().closeEvent(event)
@@ -2,11 +2,11 @@ from typing import Optional
2
2
 
3
3
  from PySide6.QtGui import QGuiApplication
4
4
  from PySide6.QtWidgets import (
5
- QWidget, QGridLayout, QLabel, QPushButton, QMessageBox, QComboBox, QCheckBox,
5
+ QWidget, QGridLayout, QLabel, QPushButton, QMessageBox, QComboBox, QCheckBox, QFileDialog,
6
6
  )
7
7
 
8
8
  from frontengine.show.pet.desktop_pet import (
9
- DesktopPetWidget, BEHAVIOUR_FLOOR, BEHAVIOUR_WANDER, BEHAVIOUR_CHASE,
9
+ DesktopPetWidget, BEHAVIOUR_FLOOR, BEHAVIOUR_WANDER, BEHAVIOUR_CHASE, scan_pet_pack,
10
10
  )
11
11
  from frontengine.ui.dialog.choose_file_dialog import choose_pet
12
12
  from frontengine.ui.page.utils import (
@@ -38,6 +38,10 @@ class PetSettingUI(QWidget):
38
38
  language_wrapper.language_word_dict.get("pet_choose_file", "Choose pet sprite")
39
39
  )
40
40
  self.choose_file_button.clicked.connect(self.choose_and_play)
41
+ self.choose_pack_button = QPushButton(
42
+ language_wrapper.language_word_dict.get("pet_choose_pack", "Choose pet pack...")
43
+ )
44
+ self.choose_pack_button.clicked.connect(self.choose_pack)
41
45
  self.ready_label = QLabel(language_wrapper.language_word_dict.get("Not Ready"))
42
46
 
43
47
  # Size
@@ -65,6 +69,8 @@ class PetSettingUI(QWidget):
65
69
  # Climb walls / ceiling (only meaningful in floor mode)
66
70
  self.climb_checkbox = QCheckBox(language_wrapper.language_word_dict.get("pet_climb_label", "Climb walls"))
67
71
  self.climb_checkbox.setChecked(True)
72
+ self.talk_checkbox = QCheckBox(language_wrapper.language_word_dict.get("pet_talk_label", "Speech bubbles"))
73
+ self.talk_checkbox.setChecked(True)
68
74
 
69
75
  # Start
70
76
  self.start_button = QPushButton(language_wrapper.language_word_dict.get("pet_start", "Spawn pet"))
@@ -81,6 +87,7 @@ class PetSettingUI(QWidget):
81
87
  # Layout
82
88
  self.grid_layout.addWidget(self.choose_file_button, 0, 0)
83
89
  self.grid_layout.addWidget(self.ready_label, 0, 1)
90
+ self.grid_layout.addWidget(self.choose_pack_button, 0, 2)
84
91
  self.grid_layout.addWidget(self.size_label, 1, 0)
85
92
  self.grid_layout.addWidget(self.size_combobox, 1, 1)
86
93
  self.grid_layout.addWidget(self.speed_label, 2, 0)
@@ -88,6 +95,7 @@ class PetSettingUI(QWidget):
88
95
  self.grid_layout.addWidget(self.behaviour_label, 3, 0)
89
96
  self.grid_layout.addWidget(self.behaviour_combobox, 3, 1)
90
97
  self.grid_layout.addWidget(self.climb_checkbox, 3, 2)
98
+ self.grid_layout.addWidget(self.talk_checkbox, 2, 2)
91
99
  self.grid_layout.addWidget(self.start_button, 4, 0)
92
100
  self.grid_layout.addWidget(self.recent_files_label, 5, 0)
93
101
  self.grid_layout.addWidget(self.recent_files_combobox, 5, 1)
@@ -102,6 +110,7 @@ class PetSettingUI(QWidget):
102
110
  speed=int(self.speed_combobox.currentText()),
103
111
  behaviour=self.behaviour_combobox.currentData(),
104
112
  climb=self.climb_checkbox.isChecked(),
113
+ talk=self.talk_checkbox.isChecked(),
105
114
  )
106
115
  pet.clone_requested.connect(self._spawn_pet)
107
116
  pet.set_pet_window_flag()
@@ -132,6 +141,28 @@ class PetSettingUI(QWidget):
132
141
  add_recent_file("pet", self.pet_image_path)
133
142
  reload_recent_combobox(self.recent_files_combobox, "pet")
134
143
 
144
+ def choose_pack(self) -> None:
145
+ """選擇動作包資料夾(依 walk/idle/climb/fall/drag 檔名對應狀態)。"""
146
+ front_engine_logger.info("[PetSettingUI] choose_pack")
147
+ folder = QFileDialog.getExistingDirectory(
148
+ self, language_wrapper.language_word_dict.get("pet_choose_pack", "Choose pet pack...")
149
+ )
150
+ if not folder:
151
+ return
152
+ if not scan_pet_pack(folder):
153
+ QMessageBox.warning(
154
+ self,
155
+ language_wrapper.language_word_dict.get("pet_choose_pack", "Choose pet pack..."),
156
+ language_wrapper.language_word_dict.get(
157
+ "pet_pack_empty", "No sprites (walk/idle/climb/fall/drag) found in that folder."),
158
+ )
159
+ return
160
+ self.pet_image_path = folder
161
+ self.ready_to_play = True
162
+ self.ready_label.setText(language_wrapper.language_word_dict.get("Ready"))
163
+ add_recent_file("pet", folder)
164
+ reload_recent_combobox(self.recent_files_combobox, "pet")
165
+
135
166
  def _apply_recent_file(self, _index: int = 0) -> None:
136
167
  path = self.recent_files_combobox.currentData()
137
168
  self.recent_files_combobox.setCurrentIndex(0)
@@ -156,6 +187,7 @@ class PetSettingUI(QWidget):
156
187
  "speed": self.speed_combobox.currentText(),
157
188
  "behaviour": self.behaviour_combobox.currentData(),
158
189
  "climb": self.climb_checkbox.isChecked(),
190
+ "talk": self.talk_checkbox.isChecked(),
159
191
  }
160
192
 
161
193
  def set_state(self, state: dict) -> None:
@@ -177,3 +209,5 @@ class PetSettingUI(QWidget):
177
209
  self.behaviour_combobox.setCurrentIndex(index)
178
210
  if "climb" in state:
179
211
  self.climb_checkbox.setChecked(bool(state["climb"]))
212
+ if "talk" in state:
213
+ self.talk_checkbox.setChecked(bool(state["talk"]))
@@ -225,6 +225,14 @@ english_word_dict = {
225
225
  "pet_behaviour_chase": "Chase cursor",
226
226
  "pet_climb_label": "Climb walls",
227
227
  "pet_clone": "Clone",
228
+ "pet_choose_pack": "Choose pet pack...",
229
+ "pet_pack_empty": "No sprites (walk/idle/climb/fall/drag) found in that folder.",
230
+ "pet_talk_label": "Speech bubbles",
231
+ "pet_chatter_morning": "Good morning!|Rise and shine!|Ready for the day?",
232
+ "pet_chatter_afternoon": "Good afternoon!|Taking a break?|Keep it up!",
233
+ "pet_chatter_evening": "Good evening!|How was your day?|Almost done?",
234
+ "pet_chatter_night": "It's late - get some rest!|Still up?|Sweet dreams soon~",
235
+ "pet_chatter_any": "Hi there!|(^_^)|Keep going!|You've got this!",
228
236
  "pet_start": "Spawn pet",
229
237
  "pet_message_box_text": "Pet image not found",
230
238
  }
@@ -226,6 +226,14 @@ traditional_chinese_word_dict = {
226
226
  "pet_behaviour_chase": "追逐游標",
227
227
  "pet_climb_label": "攀爬牆壁",
228
228
  "pet_clone": "複製",
229
+ "pet_choose_pack": "選擇動作包資料夾...",
230
+ "pet_pack_empty": "該資料夾找不到精靈檔(walk/idle/climb/fall/drag)。",
231
+ "pet_talk_label": "對話泡泡",
232
+ "pet_chatter_morning": "早安!|新的一天開始囉!|準備好了嗎?",
233
+ "pet_chatter_afternoon": "午安!|要不要休息一下?|繼續加油!",
234
+ "pet_chatter_evening": "晚上好!|今天過得如何?|快完成了嗎?",
235
+ "pet_chatter_night": "很晚了,早點休息!|還沒睡嗎?|祝好夢~",
236
+ "pet_chatter_any": "嗨!|(^_^)|加油!|你可以的!",
229
237
  "pet_start": "召喚寵物",
230
238
  "pet_message_box_text": "找不到寵物圖片",
231
239
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frontengine
3
- Version: 1.0.4
3
+ Version: 1.0.6
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
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "frontengine"
9
- version = "1.0.4"
9
+ version = "1.0.6"
10
10
  authors = [
11
11
  { name = "JE-Chen", email = "jechenmailman@gmail.com" },
12
12
  ]
File without changes
File without changes
File without changes