winipedia-utils 0.1.39__py3-none-any.whl → 0.1.40__py3-none-any.whl
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.
- winipedia_utils/pyside/core/py_qiodevice.py +404 -21
- winipedia_utils/pyside/ui/base/base.py +70 -12
- winipedia_utils/pyside/ui/pages/base/base.py +19 -3
- winipedia_utils/pyside/ui/pages/browser.py +9 -3
- winipedia_utils/pyside/ui/pages/player.py +52 -5
- winipedia_utils/pyside/ui/widgets/browser.py +96 -19
- winipedia_utils/pyside/ui/widgets/clickable_widget.py +32 -4
- winipedia_utils/pyside/ui/widgets/media_player.py +153 -30
- winipedia_utils/pyside/ui/widgets/notification.py +29 -8
- {winipedia_utils-0.1.39.dist-info → winipedia_utils-0.1.40.dist-info}/METADATA +1 -1
- {winipedia_utils-0.1.39.dist-info → winipedia_utils-0.1.40.dist-info}/RECORD +13 -13
- {winipedia_utils-0.1.39.dist-info → winipedia_utils-0.1.40.dist-info}/LICENSE +0 -0
- {winipedia_utils-0.1.39.dist-info → winipedia_utils-0.1.40.dist-info}/WHEEL +0 -0
@@ -14,11 +14,17 @@ class Browser(BasePage):
|
|
14
14
|
|
15
15
|
@final
|
16
16
|
def setup(self) -> None:
|
17
|
-
"""Setup the UI.
|
18
|
-
|
17
|
+
"""Setup the UI.
|
18
|
+
|
19
|
+
Initializes the browser page by adding a browser widget to the layout.
|
20
|
+
"""
|
19
21
|
self.add_brwoser()
|
20
22
|
|
21
23
|
@final
|
22
24
|
def add_brwoser(self) -> None:
|
23
|
-
"""Add a browser to
|
25
|
+
"""Add a browser to surf the web.
|
26
|
+
|
27
|
+
Creates and adds a BrowserWidget instance to the vertical layout,
|
28
|
+
enabling web browsing functionality within the page.
|
29
|
+
"""
|
24
30
|
self.browser = BrowserWidget(self.v_layout)
|
@@ -3,8 +3,12 @@
|
|
3
3
|
This module contains the player page class for the VideoVault application.
|
4
4
|
"""
|
5
5
|
|
6
|
+
from abc import abstractmethod
|
7
|
+
from collections.abc import Callable
|
6
8
|
from pathlib import Path
|
7
|
-
from typing import final
|
9
|
+
from typing import Any, final
|
10
|
+
|
11
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
8
12
|
|
9
13
|
from winipedia_utils.pyside.ui.pages.base.base import Base as BasePage
|
10
14
|
from winipedia_utils.pyside.ui.widgets.media_player import MediaPlayer
|
@@ -13,15 +17,58 @@ from winipedia_utils.pyside.ui.widgets.media_player import MediaPlayer
|
|
13
17
|
class Player(BasePage):
|
14
18
|
"""Player page for the VideoVault application."""
|
15
19
|
|
20
|
+
@abstractmethod
|
21
|
+
def start_playback(self, path: Path) -> None:
|
22
|
+
"""Start the playback.
|
23
|
+
|
24
|
+
Args:
|
25
|
+
path: The file path to start playback for.
|
26
|
+
"""
|
27
|
+
|
16
28
|
@final
|
17
29
|
def setup(self) -> None:
|
18
|
-
"""Setup the UI.
|
30
|
+
"""Setup the UI.
|
31
|
+
|
32
|
+
Initializes the media player widget and adds it to the layout.
|
33
|
+
"""
|
19
34
|
self.media_player = MediaPlayer(self.v_layout)
|
20
35
|
|
21
36
|
@final
|
22
|
-
def
|
23
|
-
|
37
|
+
def play_file_from_func(
|
38
|
+
self, play_func: Callable[..., Any], path: Path, **kwargs: Any
|
39
|
+
) -> None:
|
40
|
+
"""Play a file using the specified function.
|
41
|
+
|
42
|
+
Sets the current page to player and calls the provided play function
|
43
|
+
with the given path and additional arguments.
|
44
|
+
|
45
|
+
Args:
|
46
|
+
play_func: The function to call for playing the file.
|
47
|
+
path: The file path to play.
|
48
|
+
**kwargs: Additional keyword arguments to pass to the play function.
|
49
|
+
"""
|
24
50
|
# set current page to player
|
25
51
|
self.set_current_page(self.__class__)
|
26
52
|
# Stop current playback and clean up resources
|
27
|
-
|
53
|
+
play_func(path, **kwargs)
|
54
|
+
|
55
|
+
@final
|
56
|
+
def play_file(self, path: Path) -> None:
|
57
|
+
"""Play a regular video file.
|
58
|
+
|
59
|
+
Args:
|
60
|
+
path: The file path to play.
|
61
|
+
"""
|
62
|
+
self.play_file_from_func(self.media_player.play_file, path)
|
63
|
+
|
64
|
+
@final
|
65
|
+
def play_encrypted_file(self, path: Path, aes_gcm: AESGCM) -> None:
|
66
|
+
"""Play an encrypted video file.
|
67
|
+
|
68
|
+
Args:
|
69
|
+
path: The encrypted file path to play.
|
70
|
+
aes_gcm: The AES-GCM cipher instance for decryption.
|
71
|
+
"""
|
72
|
+
self.play_file_from_func(
|
73
|
+
self.media_player.play_encrypted_file, path, aes_gcm=aes_gcm
|
74
|
+
)
|
@@ -26,7 +26,13 @@ class Browser(QWebEngineView):
|
|
26
26
|
"""Browser class that creates a simple ready to use browser and not just a view."""
|
27
27
|
|
28
28
|
def __init__(self, parent_layout: QLayout, *args: Any, **kwargs: Any) -> None:
|
29
|
-
"""Initialize the browser.
|
29
|
+
"""Initialize the browser.
|
30
|
+
|
31
|
+
Args:
|
32
|
+
parent_layout: The parent layout to add the browser widget to.
|
33
|
+
*args: Additional positional arguments passed to parent constructor.
|
34
|
+
**kwargs: Additional keyword arguments passed to parent constructor.
|
35
|
+
"""
|
30
36
|
super().__init__(*args, **kwargs)
|
31
37
|
self.parent_layout = parent_layout
|
32
38
|
self.make_widget()
|
@@ -34,7 +40,11 @@ class Browser(QWebEngineView):
|
|
34
40
|
self.load_first_url()
|
35
41
|
|
36
42
|
def make_address_bar(self) -> None:
|
37
|
-
"""Make the address bar.
|
43
|
+
"""Make the address bar.
|
44
|
+
|
45
|
+
Creates a horizontal layout containing back button, forward button,
|
46
|
+
address input field, and go button for browser navigation.
|
47
|
+
"""
|
38
48
|
self.address_bar_layout = QHBoxLayout()
|
39
49
|
|
40
50
|
# Add back button
|
@@ -65,12 +75,19 @@ class Browser(QWebEngineView):
|
|
65
75
|
self.browser_layout.addLayout(self.address_bar_layout)
|
66
76
|
|
67
77
|
def navigate_to_url(self) -> None:
|
68
|
-
"""Navigate to the URL entered in the address bar.
|
78
|
+
"""Navigate to the URL entered in the address bar.
|
79
|
+
|
80
|
+
Takes the URL from the address bar text field and loads it in the browser.
|
81
|
+
"""
|
69
82
|
url = self.address_bar.text()
|
70
83
|
self.load(QUrl(url))
|
71
84
|
|
72
85
|
def make_widget(self) -> None:
|
73
|
-
"""Make the widget.
|
86
|
+
"""Make the widget.
|
87
|
+
|
88
|
+
Creates the main browser widget with vertical layout, sets size policy,
|
89
|
+
creates the address bar, and adds components to the parent layout.
|
90
|
+
"""
|
74
91
|
self.browser_widget = QWidget()
|
75
92
|
self.browser_layout = QVBoxLayout(self.browser_widget)
|
76
93
|
self.set_size_policy()
|
@@ -79,53 +96,99 @@ class Browser(QWebEngineView):
|
|
79
96
|
self.parent_layout.addWidget(self.browser_widget)
|
80
97
|
|
81
98
|
def set_size_policy(self) -> None:
|
82
|
-
"""Set the size policy.
|
99
|
+
"""Set the size policy.
|
100
|
+
|
101
|
+
Sets the browser to expand in both horizontal and vertical directions.
|
102
|
+
"""
|
83
103
|
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
84
104
|
|
85
105
|
def connect_signals(self) -> None:
|
86
|
-
"""Connect the signals.
|
106
|
+
"""Connect the signals.
|
107
|
+
|
108
|
+
Connects load finished signal and cookie added signal handlers.
|
109
|
+
"""
|
87
110
|
self.connect_load_finished_signal()
|
88
111
|
self.connect_on_cookie_added_signal()
|
89
112
|
|
90
113
|
def connect_load_finished_signal(self) -> None:
|
91
|
-
"""Connect the load finished signal.
|
92
|
-
self.loadFinished.connect(self.on_load_finished)
|
114
|
+
"""Connect the load finished signal.
|
93
115
|
|
94
|
-
|
95
|
-
"""
|
116
|
+
Connects the loadStarted signal to the on_load_started handler.
|
117
|
+
"""
|
118
|
+
self.loadStarted.connect(self.on_load_started)
|
119
|
+
|
120
|
+
def on_load_started(self, _ok: bool) -> None: # noqa: FBT001
|
121
|
+
"""Handle the load finished signal.
|
122
|
+
|
123
|
+
Args:
|
124
|
+
_ok: Boolean indicating if the load was successful (unused).
|
125
|
+
"""
|
96
126
|
self.update_address_bar(self.url())
|
97
127
|
|
98
128
|
def update_address_bar(self, url: QUrl) -> None:
|
99
|
-
"""Update the address bar with the current URL.
|
129
|
+
"""Update the address bar with the current URL.
|
130
|
+
|
131
|
+
Args:
|
132
|
+
url: The QUrl to display in the address bar.
|
133
|
+
"""
|
100
134
|
self.address_bar.setText(url.toString())
|
101
135
|
|
102
136
|
def connect_on_cookie_added_signal(self) -> None:
|
103
|
-
"""Connect the on cookie added signal.
|
137
|
+
"""Connect the on cookie added signal.
|
138
|
+
|
139
|
+
Initializes the cookies dictionary and connects the cookieAdded signal
|
140
|
+
to the on_cookie_added handler.
|
141
|
+
"""
|
104
142
|
self.cookies: dict[str, list[QNetworkCookie]] = defaultdict(list)
|
105
143
|
self.page().profile().cookieStore().cookieAdded.connect(self.on_cookie_added)
|
106
144
|
|
107
145
|
def on_cookie_added(self, cookie: Any) -> None:
|
108
|
-
"""Handle the on cookie added signal.
|
146
|
+
"""Handle the on cookie added signal.
|
147
|
+
|
148
|
+
Args:
|
149
|
+
cookie: The QNetworkCookie that was added.
|
150
|
+
"""
|
109
151
|
self.cookies[cookie.domain()].append(cookie)
|
110
152
|
|
111
153
|
def load_first_url(self) -> None:
|
112
|
-
"""Load the first URL.
|
154
|
+
"""Load the first URL.
|
155
|
+
|
156
|
+
Loads Google's homepage as the initial page when the browser starts.
|
157
|
+
"""
|
113
158
|
self.load(QUrl("https://www.google.com/"))
|
114
159
|
|
115
160
|
@property
|
116
161
|
def http_cookies(self) -> dict[str, list[Cookie]]:
|
117
|
-
"""Get the http cookies for
|
162
|
+
"""Get the http cookies for all domains.
|
163
|
+
|
164
|
+
Returns:
|
165
|
+
Dictionary mapping domain names to lists of http.cookiejar.Cookie objects.
|
166
|
+
"""
|
118
167
|
return {
|
119
168
|
domain: self.qcookies_to_httpcookies(qcookies)
|
120
169
|
for domain, qcookies in self.cookies.items()
|
121
170
|
}
|
122
171
|
|
123
172
|
def qcookies_to_httpcookies(self, qcookies: list[QNetworkCookie]) -> list[Cookie]:
|
124
|
-
"""Convert a list of QNetworkCookies to
|
173
|
+
"""Convert a list of QNetworkCookies to http.cookiejar.Cookie objects.
|
174
|
+
|
175
|
+
Args:
|
176
|
+
qcookies: List of QNetworkCookie objects to convert.
|
177
|
+
|
178
|
+
Returns:
|
179
|
+
List of converted http.cookiejar.Cookie objects.
|
180
|
+
"""
|
125
181
|
return [self.qcookie_to_httpcookie(q_cookie) for q_cookie in qcookies]
|
126
182
|
|
127
183
|
def qcookie_to_httpcookie(self, qcookie: QNetworkCookie) -> Cookie:
|
128
|
-
"""Convert a QNetworkCookie to a http.cookiejar.Cookie.
|
184
|
+
"""Convert a QNetworkCookie to a http.cookiejar.Cookie.
|
185
|
+
|
186
|
+
Args:
|
187
|
+
qcookie: The QNetworkCookie to convert.
|
188
|
+
|
189
|
+
Returns:
|
190
|
+
The converted http.cookiejar.Cookie object.
|
191
|
+
"""
|
129
192
|
name = bytes(qcookie.name().data()).decode()
|
130
193
|
value = bytes(qcookie.value().data()).decode()
|
131
194
|
domain = qcookie.domain()
|
@@ -157,10 +220,24 @@ class Browser(QWebEngineView):
|
|
157
220
|
)
|
158
221
|
|
159
222
|
def get_domain_cookies(self, domain: str) -> list[QNetworkCookie]:
|
160
|
-
"""Get the cookies for the given domain.
|
223
|
+
"""Get the cookies for the given domain.
|
224
|
+
|
225
|
+
Args:
|
226
|
+
domain: The domain to get cookies for.
|
227
|
+
|
228
|
+
Returns:
|
229
|
+
List of QNetworkCookie objects for the specified domain.
|
230
|
+
"""
|
161
231
|
return self.cookies[domain]
|
162
232
|
|
163
233
|
def get_domain_http_cookies(self, domain: str) -> list[Cookie]:
|
164
|
-
"""Get the http cookies for the given domain.
|
234
|
+
"""Get the http cookies for the given domain.
|
235
|
+
|
236
|
+
Args:
|
237
|
+
domain: The domain to get cookies for.
|
238
|
+
|
239
|
+
Returns:
|
240
|
+
List of http.cookiejar.Cookie objects for the specified domain.
|
241
|
+
"""
|
165
242
|
cookies = self.get_domain_cookies(domain)
|
166
243
|
return self.qcookies_to_httpcookies(cookies)
|
@@ -1,3 +1,9 @@
|
|
1
|
+
"""Clickable widget module.
|
2
|
+
|
3
|
+
This module contains clickable widget classes that emit signals when clicked.
|
4
|
+
Provides both regular QWidget and QVideoWidget variants with click functionality.
|
5
|
+
"""
|
6
|
+
|
1
7
|
from typing import Any
|
2
8
|
|
3
9
|
from PySide6.QtCore import Qt, Signal
|
@@ -6,24 +12,46 @@ from PySide6.QtWidgets import QWidget
|
|
6
12
|
|
7
13
|
|
8
14
|
class ClickableWidget(QWidget):
|
9
|
-
"""Widget that can be clicked.
|
15
|
+
"""Widget that can be clicked.
|
16
|
+
|
17
|
+
A QWidget subclass that emits a clicked signal when the left mouse
|
18
|
+
button is pressed on the widget.
|
19
|
+
"""
|
10
20
|
|
11
21
|
clicked = Signal()
|
12
22
|
|
13
23
|
def mousePressEvent(self, event: Any) -> None: # noqa: N802
|
14
|
-
"""Handle mouse press event.
|
24
|
+
"""Handle mouse press event.
|
25
|
+
|
26
|
+
Emits the clicked signal when the left mouse button is pressed
|
27
|
+
and passes the event to the parent class.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
event: The mouse press event containing button and position information.
|
31
|
+
"""
|
15
32
|
if event.button() == Qt.MouseButton.LeftButton:
|
16
33
|
self.clicked.emit()
|
17
34
|
super().mousePressEvent(event)
|
18
35
|
|
19
36
|
|
20
37
|
class ClickableVideoWidget(QVideoWidget):
|
21
|
-
"""Video widget that can be clicked.
|
38
|
+
"""Video widget that can be clicked.
|
39
|
+
|
40
|
+
A QVideoWidget subclass that emits a clicked signal when the left mouse
|
41
|
+
button is pressed on the video widget.
|
42
|
+
"""
|
22
43
|
|
23
44
|
clicked = Signal()
|
24
45
|
|
25
46
|
def mousePressEvent(self, event: Any) -> None: # noqa: N802
|
26
|
-
"""Handle mouse press event.
|
47
|
+
"""Handle mouse press event.
|
48
|
+
|
49
|
+
Emits the clicked signal when the left mouse button is pressed
|
50
|
+
and passes the event to the parent class.
|
51
|
+
|
52
|
+
Args:
|
53
|
+
event: The mouse press event containing button and position information.
|
54
|
+
"""
|
27
55
|
if event.button() == Qt.MouseButton.LeftButton:
|
28
56
|
self.clicked.emit()
|
29
57
|
super().mousePressEvent(event)
|
@@ -9,6 +9,7 @@ from functools import partial
|
|
9
9
|
from pathlib import Path
|
10
10
|
from typing import Any
|
11
11
|
|
12
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
12
13
|
from PySide6.QtCore import Qt, QTimer, QUrl
|
13
14
|
from PySide6.QtMultimedia import QAudioOutput, QMediaPlayer
|
14
15
|
from PySide6.QtWidgets import (
|
@@ -22,7 +23,11 @@ from PySide6.QtWidgets import (
|
|
22
23
|
QWidget,
|
23
24
|
)
|
24
25
|
|
25
|
-
from winipedia_utils.pyside.core.py_qiodevice import
|
26
|
+
from winipedia_utils.pyside.core.py_qiodevice import (
|
27
|
+
EncryptedPyQFile,
|
28
|
+
PyQFile,
|
29
|
+
PyQIODevice,
|
30
|
+
)
|
26
31
|
from winipedia_utils.pyside.ui.base.base import Base as BaseUI
|
27
32
|
from winipedia_utils.pyside.ui.widgets.clickable_widget import ClickableVideoWidget
|
28
33
|
|
@@ -31,13 +36,23 @@ class MediaPlayer(QMediaPlayer):
|
|
31
36
|
"""Media player class."""
|
32
37
|
|
33
38
|
def __init__(self, parent_layout: QLayout, *args: Any, **kwargs: Any) -> None:
|
34
|
-
"""Initialize the media player.
|
39
|
+
"""Initialize the media player.
|
40
|
+
|
41
|
+
Args:
|
42
|
+
parent_layout: The parent layout to add the media player widget to.
|
43
|
+
*args: Additional positional arguments passed to parent constructor.
|
44
|
+
**kwargs: Additional keyword arguments passed to parent constructor.
|
45
|
+
"""
|
35
46
|
super().__init__(*args, **kwargs)
|
36
47
|
self.parent_layout = parent_layout
|
37
48
|
self.make_widget()
|
38
49
|
|
39
50
|
def make_widget(self) -> None:
|
40
|
-
"""Make the widget.
|
51
|
+
"""Make the widget.
|
52
|
+
|
53
|
+
Creates the main media player widget with vertical layout, adds media controls
|
54
|
+
above and below the video widget, and creates the video widget itself.
|
55
|
+
"""
|
41
56
|
self.media_player_widget = QWidget()
|
42
57
|
self.media_player_layout = QVBoxLayout(self.media_player_widget)
|
43
58
|
self.parent_layout.addWidget(self.media_player_widget)
|
@@ -46,7 +61,11 @@ class MediaPlayer(QMediaPlayer):
|
|
46
61
|
self.add_media_controls_below()
|
47
62
|
|
48
63
|
def make_video_widget(self) -> None:
|
49
|
-
"""Make the video widget.
|
64
|
+
"""Make the video widget.
|
65
|
+
|
66
|
+
Creates a clickable video widget with expanding size policy, sets up
|
67
|
+
audio output, and connects the click signal to toggle media controls.
|
68
|
+
"""
|
50
69
|
self.video_widget = ClickableVideoWidget()
|
51
70
|
self.video_widget.clicked.connect(self.on_video_clicked)
|
52
71
|
self.video_widget.setSizePolicy(
|
@@ -60,24 +79,37 @@ class MediaPlayer(QMediaPlayer):
|
|
60
79
|
self.media_player_layout.addWidget(self.video_widget)
|
61
80
|
|
62
81
|
def on_video_clicked(self) -> None:
|
63
|
-
"""Handle video widget click.
|
82
|
+
"""Handle video widget click.
|
83
|
+
|
84
|
+
Toggles the visibility of media controls when the video widget is clicked.
|
85
|
+
"""
|
64
86
|
if self.media_controls_widget_above.isVisible():
|
65
87
|
self.hide_media_controls()
|
66
88
|
return
|
67
89
|
self.show_media_controls()
|
68
90
|
|
69
91
|
def show_media_controls(self) -> None:
|
70
|
-
"""Show media controls.
|
92
|
+
"""Show media controls.
|
93
|
+
|
94
|
+
Makes both the above and below media control widgets visible.
|
95
|
+
"""
|
71
96
|
self.media_controls_widget_above.show()
|
72
97
|
self.media_controls_widget_below.show()
|
73
98
|
|
74
99
|
def hide_media_controls(self) -> None:
|
75
|
-
"""Hide media controls.
|
100
|
+
"""Hide media controls.
|
101
|
+
|
102
|
+
Hides both the above and below media control widgets.
|
103
|
+
"""
|
76
104
|
self.media_controls_widget_above.hide()
|
77
105
|
self.media_controls_widget_below.hide()
|
78
106
|
|
79
107
|
def add_media_controls_above(self) -> None:
|
80
|
-
"""Add media controls above the video.
|
108
|
+
"""Add media controls above the video.
|
109
|
+
|
110
|
+
Creates the top control bar with left, center, and right sections,
|
111
|
+
then adds speed, volume, playback, and fullscreen controls.
|
112
|
+
"""
|
81
113
|
# main above widget
|
82
114
|
self.media_controls_widget_above = QWidget()
|
83
115
|
self.media_controls_layout_above = QHBoxLayout(self.media_controls_widget_above)
|
@@ -106,14 +138,21 @@ class MediaPlayer(QMediaPlayer):
|
|
106
138
|
self.add_fullscreen_control()
|
107
139
|
|
108
140
|
def add_media_controls_below(self) -> None:
|
109
|
-
"""Add media controls below the video.
|
141
|
+
"""Add media controls below the video.
|
142
|
+
|
143
|
+
Creates the bottom control bar and adds the progress control slider.
|
144
|
+
"""
|
110
145
|
self.media_controls_widget_below = QWidget()
|
111
146
|
self.media_controls_layout_below = QHBoxLayout(self.media_controls_widget_below)
|
112
147
|
self.media_player_layout.addWidget(self.media_controls_widget_below)
|
113
148
|
self.add_progress_control()
|
114
149
|
|
115
150
|
def add_playback_control(self) -> None:
|
116
|
-
"""Add playback control.
|
151
|
+
"""Add playback control.
|
152
|
+
|
153
|
+
Creates a play/pause button with appropriate icons and connects it
|
154
|
+
to the toggle_playback method. Adds the button to the center controls.
|
155
|
+
"""
|
117
156
|
self.play_icon = BaseUI.get_svg_icon("play_icon")
|
118
157
|
self.pause_icon = BaseUI.get_svg_icon("pause_icon")
|
119
158
|
# Pause symbol: ⏸ (U+23F8)
|
@@ -124,7 +163,11 @@ class MediaPlayer(QMediaPlayer):
|
|
124
163
|
self.center_controls_layout.addWidget(self.playback_button)
|
125
164
|
|
126
165
|
def toggle_playback(self) -> None:
|
127
|
-
"""Toggle playback.
|
166
|
+
"""Toggle playback.
|
167
|
+
|
168
|
+
Switches between play and pause states, updating the button icon
|
169
|
+
accordingly based on the current playback state.
|
170
|
+
"""
|
128
171
|
if self.playbackState() == QMediaPlayer.PlaybackState.PlayingState:
|
129
172
|
self.pause()
|
130
173
|
self.playback_button.setIcon(self.play_icon)
|
@@ -135,7 +178,8 @@ class MediaPlayer(QMediaPlayer):
|
|
135
178
|
def add_speed_control(self) -> None:
|
136
179
|
"""Add speed control.
|
137
180
|
|
138
|
-
|
181
|
+
Creates a button in the top left that shows a dropdown menu to select
|
182
|
+
playback speed from predefined options (0.2x to 5x).
|
139
183
|
"""
|
140
184
|
self.default_speed = 1
|
141
185
|
self.speed_options = [0.2, 0.5, self.default_speed, 1.5, 2, 3, 4, 5]
|
@@ -149,24 +193,40 @@ class MediaPlayer(QMediaPlayer):
|
|
149
193
|
self.left_controls_layout.addWidget(self.speed_button)
|
150
194
|
|
151
195
|
def change_speed(self, speed: float) -> None:
|
152
|
-
"""Change playback speed.
|
196
|
+
"""Change playback speed.
|
197
|
+
|
198
|
+
Args:
|
199
|
+
speed: The new playback speed multiplier.
|
200
|
+
"""
|
153
201
|
self.setPlaybackRate(speed)
|
154
202
|
self.speed_button.setText(f"{speed}x")
|
155
203
|
|
156
204
|
def add_volume_control(self) -> None:
|
157
|
-
"""Add volume control.
|
205
|
+
"""Add volume control.
|
206
|
+
|
207
|
+
Creates a horizontal slider for volume control with range 0-100
|
208
|
+
and connects it to the volume change handler.
|
209
|
+
"""
|
158
210
|
self.volume_slider = QSlider(Qt.Orientation.Horizontal)
|
159
211
|
self.volume_slider.setRange(0, 100)
|
160
212
|
self.volume_slider.valueChanged.connect(self.on_volume_changed)
|
161
213
|
self.left_controls_layout.addWidget(self.volume_slider)
|
162
214
|
|
163
215
|
def on_volume_changed(self, value: int) -> None:
|
164
|
-
"""Handle volume slider value change.
|
216
|
+
"""Handle volume slider value change.
|
217
|
+
|
218
|
+
Args:
|
219
|
+
value: The new volume value from 0-100.
|
220
|
+
"""
|
165
221
|
volume = value / 100.0 # Convert to 0.0-1.0 range
|
166
222
|
self.audio_output.setVolume(volume)
|
167
223
|
|
168
224
|
def add_fullscreen_control(self) -> None:
|
169
|
-
"""Add fullscreen control.
|
225
|
+
"""Add fullscreen control.
|
226
|
+
|
227
|
+
Creates a fullscreen toggle button with appropriate icons and determines
|
228
|
+
which widgets to hide/show when entering/exiting fullscreen mode.
|
229
|
+
"""
|
170
230
|
self.fullscreen_icon = BaseUI.get_svg_icon("fullscreen_icon")
|
171
231
|
self.exit_fullscreen_icon = BaseUI.get_svg_icon("exit_fullscreen_icon")
|
172
232
|
self.fullscreen_button = QPushButton()
|
@@ -187,7 +247,11 @@ class MediaPlayer(QMediaPlayer):
|
|
187
247
|
self.right_controls_layout.addWidget(self.fullscreen_button)
|
188
248
|
|
189
249
|
def toggle_fullscreen(self) -> None:
|
190
|
-
"""Toggle fullscreen mode.
|
250
|
+
"""Toggle fullscreen mode.
|
251
|
+
|
252
|
+
Switches between fullscreen and windowed mode, hiding/showing other
|
253
|
+
widgets and updating the button icon accordingly.
|
254
|
+
"""
|
191
255
|
# Get the main window
|
192
256
|
main_window = self.media_player_widget.window()
|
193
257
|
if main_window.isFullScreen():
|
@@ -203,7 +267,11 @@ class MediaPlayer(QMediaPlayer):
|
|
203
267
|
self.fullscreen_button.setIcon(self.exit_fullscreen_icon)
|
204
268
|
|
205
269
|
def add_progress_control(self) -> None:
|
206
|
-
"""Add progress control.
|
270
|
+
"""Add progress control.
|
271
|
+
|
272
|
+
Creates a horizontal progress slider and connects it to media player
|
273
|
+
signals for position updates and user interaction handling.
|
274
|
+
"""
|
207
275
|
self.progress_slider = QSlider(Qt.Orientation.Horizontal)
|
208
276
|
self.media_controls_layout_below.addWidget(self.progress_slider)
|
209
277
|
|
@@ -218,17 +286,32 @@ class MediaPlayer(QMediaPlayer):
|
|
218
286
|
self.progress_slider.sliderReleased.connect(self.on_slider_released)
|
219
287
|
|
220
288
|
def update_slider_position(self, position: int) -> None:
|
221
|
-
"""Update the progress slider position.
|
289
|
+
"""Update the progress slider position.
|
290
|
+
|
291
|
+
Args:
|
292
|
+
position: The current media position in milliseconds.
|
293
|
+
"""
|
222
294
|
# Only update if not being dragged to prevent jumps during manual sliding
|
223
295
|
if not self.progress_slider.isSliderDown():
|
224
296
|
self.progress_slider.setValue(position)
|
225
297
|
|
226
298
|
def set_slider_range(self, duration: int) -> None:
|
227
|
-
"""Set the progress slider range based on media duration.
|
299
|
+
"""Set the progress slider range based on media duration.
|
300
|
+
|
301
|
+
Args:
|
302
|
+
duration: The total media duration in milliseconds.
|
303
|
+
"""
|
228
304
|
self.progress_slider.setRange(0, duration)
|
229
305
|
|
230
306
|
def on_slider_moved(self, position: int) -> None:
|
231
|
-
"""Set the media position when slider is moved.
|
307
|
+
"""Set the media position when slider is moved.
|
308
|
+
|
309
|
+
Implements throttling to prevent excessive position updates during
|
310
|
+
slider dragging for better performance.
|
311
|
+
|
312
|
+
Args:
|
313
|
+
position: The new position from the slider in milliseconds.
|
314
|
+
"""
|
232
315
|
current_time = time.time()
|
233
316
|
if (
|
234
317
|
current_time - self.last_slider_moved_update
|
@@ -238,13 +321,25 @@ class MediaPlayer(QMediaPlayer):
|
|
238
321
|
self.last_slider_moved_update = current_time
|
239
322
|
|
240
323
|
def on_slider_released(self) -> None:
|
241
|
-
"""Handle slider release event.
|
324
|
+
"""Handle slider release event.
|
325
|
+
|
326
|
+
Sets the final media position when the user releases the slider.
|
327
|
+
"""
|
242
328
|
self.setPosition(self.progress_slider.value())
|
243
329
|
|
244
330
|
def play_video(
|
245
331
|
self, set_source_func: Callable[..., Any], *args: Any, **kwargs: Any
|
246
332
|
) -> None:
|
247
|
-
"""Play the video.
|
333
|
+
"""Play the video.
|
334
|
+
|
335
|
+
Stops current playback and starts a new video using the provided
|
336
|
+
source function with a delay to prevent freezing.
|
337
|
+
|
338
|
+
Args:
|
339
|
+
set_source_func: Function to call for setting the video source.
|
340
|
+
*args: Additional positional arguments for the source function.
|
341
|
+
**kwargs: Additional keyword arguments for the source function.
|
342
|
+
"""
|
248
343
|
self.stop()
|
249
344
|
|
250
345
|
# prevents freezing when starting a new video while another is playing
|
@@ -255,20 +350,48 @@ class MediaPlayer(QMediaPlayer):
|
|
255
350
|
def set_source_and_play(
|
256
351
|
self, set_source_func: Callable[..., Any], *args: Any, **kwargs: Any
|
257
352
|
) -> None:
|
258
|
-
"""Set the source and play the video.
|
353
|
+
"""Set the source and play the video.
|
354
|
+
|
355
|
+
Args:
|
356
|
+
set_source_func: Function to call for setting the video source.
|
357
|
+
*args: Additional positional arguments for the source function.
|
358
|
+
**kwargs: Additional keyword arguments for the source function.
|
359
|
+
"""
|
259
360
|
set_source_func(*args, **kwargs)
|
260
361
|
self.play()
|
261
362
|
|
363
|
+
def set_source_device(self, io_device: PyQIODevice, source_url: QUrl) -> None:
|
364
|
+
"""Set the source device for playback.
|
365
|
+
|
366
|
+
Args:
|
367
|
+
io_device: The PyQIODevice to use as the media source.
|
368
|
+
source_url: The QUrl representing the source location.
|
369
|
+
"""
|
370
|
+
self.source_url = source_url
|
371
|
+
self.io_device = io_device
|
372
|
+
self.setSourceDevice(self.io_device, self.source_url)
|
373
|
+
|
262
374
|
def play_file(self, path: Path) -> None:
|
263
|
-
"""Play
|
375
|
+
"""Play a regular video file.
|
376
|
+
|
377
|
+
Args:
|
378
|
+
path: The file path to the video file to play.
|
379
|
+
"""
|
264
380
|
self.play_video(
|
265
381
|
self.set_source_device,
|
266
382
|
io_device=PyQFile(path),
|
267
383
|
source_url=QUrl.fromLocalFile(path),
|
268
384
|
)
|
269
385
|
|
270
|
-
def
|
271
|
-
"""Play
|
272
|
-
|
273
|
-
|
274
|
-
|
386
|
+
def play_encrypted_file(self, path: Path, aes_gcm: AESGCM) -> None:
|
387
|
+
"""Play an encrypted video file.
|
388
|
+
|
389
|
+
Args:
|
390
|
+
path: The file path to the encrypted video file to play.
|
391
|
+
aes_gcm: The AES-GCM cipher instance for decryption.
|
392
|
+
"""
|
393
|
+
self.play_video(
|
394
|
+
self.set_source_device,
|
395
|
+
io_device=EncryptedPyQFile(path, aes_gcm),
|
396
|
+
source_url=QUrl.fromLocalFile(path),
|
397
|
+
)
|