winipedia-pyside 0.2.0__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.
Potentially problematic release.
This version of winipedia-pyside might be problematic. Click here for more details.
- winipedia_pyside/__init__.py +1 -0
- winipedia_pyside/core/__init__.py +1 -0
- winipedia_pyside/core/py_qiodevice.py +476 -0
- winipedia_pyside/py.typed +0 -0
- winipedia_pyside/ui/__init__.py +1 -0
- winipedia_pyside/ui/base/__init__.py +1 -0
- winipedia_pyside/ui/base/base.py +181 -0
- winipedia_pyside/ui/pages/__init__.py +1 -0
- winipedia_pyside/ui/pages/base/__init__.py +1 -0
- winipedia_pyside/ui/pages/base/base.py +92 -0
- winipedia_pyside/ui/pages/browser.py +26 -0
- winipedia_pyside/ui/pages/player.py +85 -0
- winipedia_pyside/ui/widgets/__init__.py +1 -0
- winipedia_pyside/ui/widgets/browser.py +243 -0
- winipedia_pyside/ui/widgets/clickable_widget.py +57 -0
- winipedia_pyside/ui/widgets/media_player.py +430 -0
- winipedia_pyside/ui/widgets/notification.py +77 -0
- winipedia_pyside/ui/windows/__init__.py +1 -0
- winipedia_pyside/ui/windows/base/__init__.py +1 -0
- winipedia_pyside/ui/windows/base/base.py +49 -0
- winipedia_pyside-0.2.0.dist-info/METADATA +20 -0
- winipedia_pyside-0.2.0.dist-info/RECORD +24 -0
- winipedia_pyside-0.2.0.dist-info/WHEEL +4 -0
- winipedia_pyside-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""__init__ module."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""__init__ module for winipedia_pyside6.core."""
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
"""PySide6 QIODevice wrapper."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from collections.abc import Generator
|
|
5
|
+
from functools import partial
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
10
|
+
from PySide6.QtCore import QFile, QIODevice
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PyQIODevice(QIODevice):
|
|
14
|
+
"""PySide6 QIODevice wrapper with enhanced functionality.
|
|
15
|
+
|
|
16
|
+
A wrapper class that provides a Python-friendly interface to PySide6's QIODevice,
|
|
17
|
+
allowing for easier integration with Python code while maintaining all the
|
|
18
|
+
original functionality.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
QIODevice: The base QIODevice class from PySide6.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, q_device: QIODevice, *args: Any, **kwargs: Any) -> None:
|
|
25
|
+
"""Initialize the PyQIODevice wrapper.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
q_device: The QIODevice instance to wrap.
|
|
29
|
+
*args: Additional positional arguments passed to parent constructor.
|
|
30
|
+
**kwargs: Additional keyword arguments passed to parent constructor.
|
|
31
|
+
"""
|
|
32
|
+
super().__init__(*args, **kwargs)
|
|
33
|
+
self.q_device = q_device
|
|
34
|
+
|
|
35
|
+
def atEnd(self) -> bool: # noqa: N802
|
|
36
|
+
"""Check if the device is at the end of data.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
True if the device is at the end, False otherwise.
|
|
40
|
+
"""
|
|
41
|
+
return self.q_device.atEnd()
|
|
42
|
+
|
|
43
|
+
def bytesAvailable(self) -> int: # noqa: N802
|
|
44
|
+
"""Get the number of bytes available for reading.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The number of bytes available for reading.
|
|
48
|
+
"""
|
|
49
|
+
return self.q_device.bytesAvailable()
|
|
50
|
+
|
|
51
|
+
def bytesToWrite(self) -> int: # noqa: N802
|
|
52
|
+
"""Get the number of bytes waiting to be written.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
The number of bytes waiting to be written.
|
|
56
|
+
"""
|
|
57
|
+
return self.q_device.bytesToWrite()
|
|
58
|
+
|
|
59
|
+
def canReadLine(self) -> bool: # noqa: N802
|
|
60
|
+
"""Check if a complete line can be read from the device.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
True if a complete line can be read, False otherwise.
|
|
64
|
+
"""
|
|
65
|
+
return self.q_device.canReadLine()
|
|
66
|
+
|
|
67
|
+
def close(self) -> None:
|
|
68
|
+
"""Close the device and release resources.
|
|
69
|
+
|
|
70
|
+
Closes the underlying QIODevice and calls the parent close method.
|
|
71
|
+
"""
|
|
72
|
+
self.q_device.close()
|
|
73
|
+
return super().close()
|
|
74
|
+
|
|
75
|
+
def isSequential(self) -> bool: # noqa: N802
|
|
76
|
+
"""Check if the device is sequential.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
True if the device is sequential, False if it supports random access.
|
|
80
|
+
"""
|
|
81
|
+
return self.q_device.isSequential()
|
|
82
|
+
|
|
83
|
+
def open(self, mode: QIODevice.OpenModeFlag) -> bool:
|
|
84
|
+
"""Open the device with the specified mode.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
mode: The open mode flag specifying how to open the device.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
True if the device was opened successfully, False otherwise.
|
|
91
|
+
"""
|
|
92
|
+
self.q_device.open(mode)
|
|
93
|
+
return super().open(mode)
|
|
94
|
+
|
|
95
|
+
def pos(self) -> int:
|
|
96
|
+
"""Get the current position in the device.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
The current position in the device.
|
|
100
|
+
"""
|
|
101
|
+
return self.q_device.pos()
|
|
102
|
+
|
|
103
|
+
def readData(self, maxlen: int) -> bytes: # noqa: N802
|
|
104
|
+
"""Read data from the device.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
maxlen: The maximum number of bytes to read.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
The data read from the device as bytes.
|
|
111
|
+
"""
|
|
112
|
+
return bytes(self.q_device.read(maxlen).data())
|
|
113
|
+
|
|
114
|
+
def readLineData(self, maxlen: int) -> object: # noqa: N802
|
|
115
|
+
"""Read a line from the device.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
maxlen: The maximum number of bytes to read.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
The line data read from the device.
|
|
122
|
+
"""
|
|
123
|
+
return self.q_device.readLine(maxlen)
|
|
124
|
+
|
|
125
|
+
def reset(self) -> bool:
|
|
126
|
+
"""Reset the device to its initial state.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
True if the device was reset successfully, False otherwise.
|
|
130
|
+
"""
|
|
131
|
+
return self.q_device.reset()
|
|
132
|
+
|
|
133
|
+
def seek(self, pos: int) -> bool:
|
|
134
|
+
"""Seek to a specific position in the device.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
pos: The position to seek to.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
True if the seek operation was successful, False otherwise.
|
|
141
|
+
"""
|
|
142
|
+
return self.q_device.seek(pos)
|
|
143
|
+
|
|
144
|
+
def size(self) -> int:
|
|
145
|
+
"""Get the size of the device.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
The size of the device in bytes.
|
|
149
|
+
"""
|
|
150
|
+
return self.q_device.size()
|
|
151
|
+
|
|
152
|
+
def skipData(self, maxSize: int) -> int: # noqa: N802, N803
|
|
153
|
+
"""Skip data in the device.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
maxSize: The maximum number of bytes to skip.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
The actual number of bytes skipped.
|
|
160
|
+
"""
|
|
161
|
+
return self.q_device.skip(maxSize)
|
|
162
|
+
|
|
163
|
+
def waitForBytesWritten(self, msecs: int) -> bool: # noqa: N802
|
|
164
|
+
"""Wait for bytes to be written to the device.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
msecs: The maximum time to wait in milliseconds.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
True if bytes were written within the timeout, False otherwise.
|
|
171
|
+
"""
|
|
172
|
+
return self.q_device.waitForBytesWritten(msecs)
|
|
173
|
+
|
|
174
|
+
def waitForReadyRead(self, msecs: int) -> bool: # noqa: N802
|
|
175
|
+
"""Wait for the device to be ready for reading.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
msecs: The maximum time to wait in milliseconds.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
True if the device became ready within the timeout, False otherwise.
|
|
182
|
+
"""
|
|
183
|
+
return self.q_device.waitForReadyRead(msecs)
|
|
184
|
+
|
|
185
|
+
def writeData(self, data: bytes | bytearray | memoryview, len: int) -> int: # noqa: A002, ARG002, N802
|
|
186
|
+
"""Write data to the device.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
data: The data to write to the device.
|
|
190
|
+
len: The length parameter (unused in this implementation).
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
The number of bytes actually written.
|
|
194
|
+
"""
|
|
195
|
+
return self.q_device.write(data)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class PyQFile(PyQIODevice):
|
|
199
|
+
"""QFile wrapper with enhanced Python integration.
|
|
200
|
+
|
|
201
|
+
A specialized PyQIODevice wrapper for file operations, providing a more
|
|
202
|
+
Python-friendly interface to PySide6's QFile functionality.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
def __init__(self, path: Path, *args: Any, **kwargs: Any) -> None:
|
|
206
|
+
"""Initialize the PyQFile with a file path.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
path: The file path to open.
|
|
210
|
+
*args: Additional positional arguments passed to parent constructor.
|
|
211
|
+
**kwargs: Additional keyword arguments passed to parent constructor.
|
|
212
|
+
"""
|
|
213
|
+
super().__init__(QFile(path), *args, **kwargs)
|
|
214
|
+
self.q_device: QFile
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class EncryptedPyQFile(PyQFile):
|
|
218
|
+
"""Encrypted file wrapper using AES-GCM encryption.
|
|
219
|
+
|
|
220
|
+
Provides transparent encryption/decryption for file operations using
|
|
221
|
+
AES-GCM (Galois/Counter Mode) encryption. Data is encrypted in chunks
|
|
222
|
+
for efficient streaming operations.
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
NONCE_SIZE = 12
|
|
226
|
+
CIPHER_SIZE = 64 * 1024
|
|
227
|
+
TAG_SIZE = 16
|
|
228
|
+
CHUNK_SIZE = CIPHER_SIZE + NONCE_SIZE + TAG_SIZE
|
|
229
|
+
CHUNK_OVERHEAD = NONCE_SIZE + TAG_SIZE
|
|
230
|
+
|
|
231
|
+
def __init__(self, path: Path, aes_gcm: AESGCM, *args: Any, **kwargs: Any) -> None:
|
|
232
|
+
"""Initialize the encrypted file wrapper.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
path: The file path to open.
|
|
236
|
+
aes_gcm: The AES-GCM cipher instance for encryption/decryption.
|
|
237
|
+
*args: Additional positional arguments passed to parent constructor.
|
|
238
|
+
**kwargs: Additional keyword arguments passed to parent constructor.
|
|
239
|
+
"""
|
|
240
|
+
super().__init__(path, *args, **kwargs)
|
|
241
|
+
self.q_device: QFile
|
|
242
|
+
self.aes_gcm = aes_gcm
|
|
243
|
+
self.dec_size = self.size()
|
|
244
|
+
|
|
245
|
+
def readData(self, maxlen: int) -> bytes: # noqa: N802
|
|
246
|
+
"""Read and decrypt data from the encrypted file.
|
|
247
|
+
|
|
248
|
+
Reads encrypted chunks from the file, decrypts them, and returns
|
|
249
|
+
the requested portion of decrypted data.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
maxlen: The maximum number of decrypted bytes to read.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
The decrypted data as bytes.
|
|
256
|
+
"""
|
|
257
|
+
# where we are in the encrypted data
|
|
258
|
+
dec_pos = self.pos()
|
|
259
|
+
# where we are in the decrypted data
|
|
260
|
+
enc_pos = self.get_encrypted_pos(dec_pos)
|
|
261
|
+
|
|
262
|
+
# get the chunk start and end
|
|
263
|
+
chunk_start = self.get_chunk_start(enc_pos)
|
|
264
|
+
chunk_end = self.get_chunk_end(enc_pos, maxlen)
|
|
265
|
+
new_maxlen = chunk_end - chunk_start
|
|
266
|
+
|
|
267
|
+
# read the chunk
|
|
268
|
+
self.seek(chunk_start)
|
|
269
|
+
enc_data = super().readData(new_maxlen)
|
|
270
|
+
# decrypt the chunk
|
|
271
|
+
dec_data = self.decrypt_data(enc_data)
|
|
272
|
+
|
|
273
|
+
# get the start and end of the requested data in the decrypted data
|
|
274
|
+
dec_chunk_start = self.get_decrypted_pos(chunk_start + self.NONCE_SIZE)
|
|
275
|
+
|
|
276
|
+
req_data_start = dec_pos - dec_chunk_start
|
|
277
|
+
req_data_end = req_data_start + maxlen
|
|
278
|
+
|
|
279
|
+
dec_pos += maxlen
|
|
280
|
+
self.seek(dec_pos)
|
|
281
|
+
|
|
282
|
+
return dec_data[req_data_start:req_data_end]
|
|
283
|
+
|
|
284
|
+
def writeData(self, data: bytes | bytearray | memoryview, len: int) -> int: # noqa: A002, ARG002, N802
|
|
285
|
+
"""Encrypt and write data to the file.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
data: The data to encrypt and write.
|
|
289
|
+
len: The length parameter (unused in this implementation).
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
The number of bytes actually written.
|
|
293
|
+
"""
|
|
294
|
+
encrypted_data = self.encrypt_data(bytes(data))
|
|
295
|
+
encrypted_len = encrypted_data.__len__()
|
|
296
|
+
return super().writeData(encrypted_data, encrypted_len)
|
|
297
|
+
|
|
298
|
+
def size(self) -> int:
|
|
299
|
+
"""Get the decrypted size of the file.
|
|
300
|
+
|
|
301
|
+
Calculates the decrypted size based on the encrypted file size
|
|
302
|
+
and chunk structure.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
The decrypted size of the file in bytes.
|
|
306
|
+
"""
|
|
307
|
+
self.enc_size = super().size()
|
|
308
|
+
self.num_chunks = self.enc_size // self.CHUNK_SIZE + 1
|
|
309
|
+
self.dec_size = self.num_chunks * self.CIPHER_SIZE
|
|
310
|
+
return self.dec_size
|
|
311
|
+
|
|
312
|
+
def get_decrypted_pos(self, enc_pos: int) -> int:
|
|
313
|
+
"""Convert encrypted file position to decrypted position.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
enc_pos: The position in the encrypted file.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
The corresponding position in the decrypted data.
|
|
320
|
+
"""
|
|
321
|
+
if enc_pos >= self.enc_size:
|
|
322
|
+
return self.dec_size
|
|
323
|
+
|
|
324
|
+
num_chunks_before = enc_pos // self.CHUNK_SIZE
|
|
325
|
+
last_enc_chunk_start = num_chunks_before * self.CHUNK_SIZE
|
|
326
|
+
last_dec_chunk_start = num_chunks_before * self.CIPHER_SIZE
|
|
327
|
+
|
|
328
|
+
enc_bytes_to_move = enc_pos - last_enc_chunk_start
|
|
329
|
+
|
|
330
|
+
return last_dec_chunk_start + enc_bytes_to_move - self.NONCE_SIZE
|
|
331
|
+
|
|
332
|
+
def get_encrypted_pos(self, dec_pos: int) -> int:
|
|
333
|
+
"""Convert decrypted position to encrypted file position.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
dec_pos: The position in the decrypted data.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
The corresponding position in the encrypted file.
|
|
340
|
+
"""
|
|
341
|
+
if dec_pos >= self.dec_size:
|
|
342
|
+
return self.enc_size
|
|
343
|
+
num_chunks_before = dec_pos // self.CIPHER_SIZE
|
|
344
|
+
last_dec_chunk_start = num_chunks_before * self.CIPHER_SIZE
|
|
345
|
+
last_enc_chunk_start = num_chunks_before * self.CHUNK_SIZE
|
|
346
|
+
|
|
347
|
+
dec_bytes_to_move = dec_pos - last_dec_chunk_start
|
|
348
|
+
|
|
349
|
+
return last_enc_chunk_start + self.NONCE_SIZE + dec_bytes_to_move
|
|
350
|
+
|
|
351
|
+
def get_chunk_start(self, pos: int) -> int:
|
|
352
|
+
"""Get the start position of the chunk containing the given position.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
pos: The position to find the chunk start for.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
The start position of the chunk.
|
|
359
|
+
"""
|
|
360
|
+
return pos // self.CHUNK_SIZE * self.CHUNK_SIZE
|
|
361
|
+
|
|
362
|
+
def get_chunk_end(self, pos: int, maxlen: int) -> int:
|
|
363
|
+
"""Get the end position of the chunk range for the given position and length.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
pos: The starting position.
|
|
367
|
+
maxlen: The maximum length to read.
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
The end position of the chunk range.
|
|
371
|
+
"""
|
|
372
|
+
return (pos + maxlen) // self.CHUNK_SIZE * self.CHUNK_SIZE + self.CHUNK_SIZE
|
|
373
|
+
|
|
374
|
+
@classmethod
|
|
375
|
+
def chunk_generator(
|
|
376
|
+
cls, data: bytes, *, is_encrypted: bool
|
|
377
|
+
) -> Generator[bytes, None, None]:
|
|
378
|
+
"""Generate chunks from data.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
data: The data to split into chunks.
|
|
382
|
+
is_encrypted: Whether the data is encrypted (affects chunk size).
|
|
383
|
+
|
|
384
|
+
Yields:
|
|
385
|
+
Chunks of data of appropriate size.
|
|
386
|
+
"""
|
|
387
|
+
size = cls.CHUNK_SIZE if is_encrypted else cls.CIPHER_SIZE
|
|
388
|
+
for i in range(0, len(data), size):
|
|
389
|
+
yield data[i : i + size]
|
|
390
|
+
|
|
391
|
+
def encrypt_data(self, data: bytes) -> bytes:
|
|
392
|
+
"""Encrypt data using AES-GCM.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
data: The data to encrypt.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
The encrypted data with nonce and authentication tag.
|
|
399
|
+
"""
|
|
400
|
+
return self.encrypt_data_static(data, self.aes_gcm)
|
|
401
|
+
|
|
402
|
+
@classmethod
|
|
403
|
+
def encrypt_data_static(cls, data: bytes, aes_gcm: AESGCM) -> bytes:
|
|
404
|
+
"""Encrypt data using AES-GCM (static method).
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
data: The data to encrypt.
|
|
408
|
+
aes_gcm: The AES-GCM cipher instance for encryption/decryption.
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
The encrypted data with nonce and authentication tag.
|
|
412
|
+
"""
|
|
413
|
+
decrypted_chunks = cls.chunk_generator(data, is_encrypted=False)
|
|
414
|
+
encrypted_chunks = map(
|
|
415
|
+
partial(cls.encrypt_chunk_static, aes_gcm=aes_gcm), decrypted_chunks
|
|
416
|
+
)
|
|
417
|
+
return b"".join(encrypted_chunks)
|
|
418
|
+
|
|
419
|
+
@classmethod
|
|
420
|
+
def encrypt_chunk_static(cls, data: bytes, aes_gcm: AESGCM) -> bytes:
|
|
421
|
+
"""Encrypt a single chunk using AES-GCM (static method).
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
data: The chunk data to encrypt.
|
|
425
|
+
aes_gcm: The AES-GCM cipher instance for encryption/decryption.
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
The encrypted chunk with nonce and authentication tag.
|
|
429
|
+
"""
|
|
430
|
+
nonce = os.urandom(12)
|
|
431
|
+
aad = cls.__name__.encode()
|
|
432
|
+
return nonce + aes_gcm.encrypt(nonce, data, aad)
|
|
433
|
+
|
|
434
|
+
def decrypt_data(self, data: bytes) -> bytes:
|
|
435
|
+
"""Decrypt data using AES-GCM.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
data: The encrypted data to decrypt.
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
The decrypted data as bytes.
|
|
442
|
+
"""
|
|
443
|
+
return self.decrypt_data_static(data, self.aes_gcm)
|
|
444
|
+
|
|
445
|
+
@classmethod
|
|
446
|
+
def decrypt_data_static(cls, data: bytes, aes_gcm: AESGCM) -> bytes:
|
|
447
|
+
"""Decrypt data using AES-GCM (static method).
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
data: The encrypted data to decrypt.
|
|
451
|
+
aes_gcm: The AES-GCM cipher instance for encryption/decryption.
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
The decrypted data as bytes.
|
|
455
|
+
"""
|
|
456
|
+
encrypted_chunks = cls.chunk_generator(data, is_encrypted=True)
|
|
457
|
+
decrypted_chunks = map(
|
|
458
|
+
partial(cls.decrypt_chunk_static, aes_gcm=aes_gcm), encrypted_chunks
|
|
459
|
+
)
|
|
460
|
+
return b"".join(decrypted_chunks)
|
|
461
|
+
|
|
462
|
+
@classmethod
|
|
463
|
+
def decrypt_chunk_static(cls, data: bytes, aes_gcm: AESGCM) -> bytes:
|
|
464
|
+
"""Decrypt a single chunk using AES-GCM (static method).
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
data: The encrypted chunk data to decrypt.
|
|
468
|
+
aes_gcm: The AES-GCM cipher instance for encryption/decryption.
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
The decrypted chunk data as bytes.
|
|
472
|
+
"""
|
|
473
|
+
nonce = data[: cls.NONCE_SIZE]
|
|
474
|
+
cipher_and_tag = data[cls.NONCE_SIZE :]
|
|
475
|
+
aad = cls.__name__.encode()
|
|
476
|
+
return aes_gcm.decrypt(nonce, cipher_and_tag, aad)
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""__init__ module for winipedia_pyside6.ui."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""__init__ module."""
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Base UI module.
|
|
2
|
+
|
|
3
|
+
This module contains the base UI class for the VideoVault application.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from abc import abstractmethod
|
|
7
|
+
from types import ModuleType
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Self, cast
|
|
9
|
+
|
|
10
|
+
from PySide6.QtCore import QObject
|
|
11
|
+
from PySide6.QtGui import QIcon
|
|
12
|
+
from PySide6.QtWidgets import QApplication, QStackedWidget
|
|
13
|
+
from winipedia_utils.modules.class_ import (
|
|
14
|
+
get_all_nonabstract_subclasses,
|
|
15
|
+
)
|
|
16
|
+
from winipedia_utils.modules.package import get_main_package, walk_package
|
|
17
|
+
from winipedia_utils.oop.mixins.meta import ABCLoggingMeta
|
|
18
|
+
from winipedia_utils.resources.svgs.svg import get_svg_path
|
|
19
|
+
from winipedia_utils.text.string import split_on_uppercase
|
|
20
|
+
|
|
21
|
+
# Avoid circular import
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from winipedia_pyside.ui.pages.base.base import Base as BasePage
|
|
24
|
+
from winipedia_pyside.ui.windows.base.base import Base as BaseWindow
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class QABCLoggingMeta(
|
|
28
|
+
ABCLoggingMeta,
|
|
29
|
+
type(QObject), # type: ignore[misc]
|
|
30
|
+
):
|
|
31
|
+
"""Metaclass for the QABCImplementationLoggingMixin."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Base(metaclass=QABCLoggingMeta):
|
|
35
|
+
"""Base UI class for a Qt application."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
38
|
+
"""Initialize the base UI."""
|
|
39
|
+
super().__init__(*args, **kwargs)
|
|
40
|
+
self.base_setup()
|
|
41
|
+
self.pre_setup()
|
|
42
|
+
self.setup()
|
|
43
|
+
self.post_setup()
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def base_setup(self) -> None:
|
|
47
|
+
"""Setup the base Qt object of the UI.
|
|
48
|
+
|
|
49
|
+
This method should initialize the core Qt components required
|
|
50
|
+
for the UI to function properly.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def setup(self) -> None:
|
|
55
|
+
"""Setup the main UI components.
|
|
56
|
+
|
|
57
|
+
This method should contain the primary UI initialization logic.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def pre_setup(self) -> None:
|
|
62
|
+
"""Setup operations to run before main setup.
|
|
63
|
+
|
|
64
|
+
This method should contain any initialization that needs to happen
|
|
65
|
+
before the main setup method is called.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
@abstractmethod
|
|
69
|
+
def post_setup(self) -> None:
|
|
70
|
+
"""Setup operations to run after main setup.
|
|
71
|
+
|
|
72
|
+
This method should contain any finalization that needs to happen
|
|
73
|
+
after the main setup method is called.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def get_display_name(cls) -> str:
|
|
78
|
+
"""Get the display name of the UI.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
The human-readable display name derived from the class name.
|
|
82
|
+
"""
|
|
83
|
+
return " ".join(split_on_uppercase(cls.__name__))
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def get_subclasses(cls, package: ModuleType | None = None) -> list[type[Self]]:
|
|
87
|
+
"""Get all subclasses of the UI.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
package: The package to search for subclasses in. If None,
|
|
91
|
+
searches in the main package.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
A sorted list of all non-abstract subclasses.
|
|
95
|
+
"""
|
|
96
|
+
if package is None:
|
|
97
|
+
# find the main package
|
|
98
|
+
package = get_main_package()
|
|
99
|
+
|
|
100
|
+
_ = list(walk_package(package))
|
|
101
|
+
|
|
102
|
+
children = get_all_nonabstract_subclasses(cls)
|
|
103
|
+
return sorted(children, key=lambda cls: cls.__name__)
|
|
104
|
+
|
|
105
|
+
def set_current_page(self, page_cls: type["BasePage"]) -> None:
|
|
106
|
+
"""Set the current page in the stack.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
page_cls: The page class to set as current.
|
|
110
|
+
"""
|
|
111
|
+
self.get_stack().setCurrentWidget(self.get_page(page_cls))
|
|
112
|
+
|
|
113
|
+
def get_stack(self) -> QStackedWidget:
|
|
114
|
+
"""Get the stack widget of the window.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
The QStackedWidget containing all pages.
|
|
118
|
+
"""
|
|
119
|
+
window = cast("BaseWindow", (getattr(self, "window", lambda: None)()))
|
|
120
|
+
|
|
121
|
+
return window.stack
|
|
122
|
+
|
|
123
|
+
def get_stack_pages(self) -> list["BasePage"]:
|
|
124
|
+
"""Get all pages from the stack.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
A list of all pages currently in the stack.
|
|
128
|
+
"""
|
|
129
|
+
# Import here to avoid circular import
|
|
130
|
+
|
|
131
|
+
stack = self.get_stack()
|
|
132
|
+
# get all the pages
|
|
133
|
+
return [cast("BasePage", stack.widget(i)) for i in range(stack.count())]
|
|
134
|
+
|
|
135
|
+
def get_page[T: "BasePage"](self, page_cls: type[T]) -> T:
|
|
136
|
+
"""Get a specific page from the stack.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
page_cls: The class of the page to retrieve.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
The page instance of the specified class.
|
|
143
|
+
"""
|
|
144
|
+
page = next(
|
|
145
|
+
page for page in self.get_stack_pages() if page.__class__ is page_cls
|
|
146
|
+
)
|
|
147
|
+
return cast("T", page)
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def get_svg_icon(cls, svg_name: str, package: ModuleType | None = None) -> QIcon:
|
|
151
|
+
"""Get a QIcon for an SVG file.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
svg_name: The name of the SVG file.
|
|
155
|
+
package: The package to search for the SVG in. If None,
|
|
156
|
+
searches in the main package.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
A QIcon created from the SVG file.
|
|
160
|
+
"""
|
|
161
|
+
return QIcon(str(get_svg_path(svg_name, package=package)))
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def get_page_static[T: "BasePage"](cls, page_cls: type[T]) -> T:
|
|
165
|
+
"""Get a page statically from the main window.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
page_cls: The class of the page to retrieve.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
The page instance of the specified class from the main window.
|
|
172
|
+
"""
|
|
173
|
+
from winipedia_pyside.ui.windows.base.base import ( # noqa: PLC0415 bc of circular import
|
|
174
|
+
Base as BaseWindow,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
top_level_widgets = QApplication.topLevelWidgets()
|
|
178
|
+
main_window = next(
|
|
179
|
+
widget for widget in top_level_widgets if isinstance(widget, BaseWindow)
|
|
180
|
+
)
|
|
181
|
+
return main_window.get_page(page_cls)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""__init__ module."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""__init__ module."""
|