winipedia-utils 0.1.15__tar.gz → 0.1.17__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.
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/PKG-INFO +6 -2
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/pyproject.toml +7 -3
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/modules/module.py +4 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/projects/poetry/config.py +1 -1
- winipedia_utils-0.1.17/winipedia_utils/pyside/__init__.py +1 -0
- winipedia_utils-0.1.17/winipedia_utils/pyside/ui/__init__.py +1 -0
- winipedia_utils-0.1.17/winipedia_utils/pyside/ui/base/__init__.py +1 -0
- winipedia_utils-0.1.17/winipedia_utils/pyside/ui/base/base.py +117 -0
- winipedia_utils-0.1.17/winipedia_utils/pyside/ui/pages/__init__.py +1 -0
- winipedia_utils-0.1.17/winipedia_utils/pyside/ui/pages/base/__init__.py +1 -0
- winipedia_utils-0.1.17/winipedia_utils/pyside/ui/pages/base/base.py +78 -0
- winipedia_utils-0.1.17/winipedia_utils/pyside/ui/pages/browser.py +24 -0
- winipedia_utils-0.1.17/winipedia_utils/pyside/ui/pages/player.py +26 -0
- winipedia_utils-0.1.17/winipedia_utils/pyside/ui/widgets/__init__.py +1 -0
- winipedia_utils-0.1.17/winipedia_utils/pyside/ui/widgets/browser.py +166 -0
- winipedia_utils-0.1.17/winipedia_utils/pyside/ui/widgets/media_player.py +265 -0
- winipedia_utils-0.1.17/winipedia_utils/pyside/ui/widgets/notification.py +56 -0
- winipedia_utils-0.1.17/winipedia_utils/pyside/ui/windows/__init__.py +1 -0
- winipedia_utils-0.1.17/winipedia_utils/pyside/ui/windows/base/__init__.py +1 -0
- winipedia_utils-0.1.17/winipedia_utils/pyside/ui/windows/base/base.py +56 -0
- winipedia_utils-0.1.17/winipedia_utils/resources/__init__.py +1 -0
- winipedia_utils-0.1.17/winipedia_utils/resources/svgs/__init__.py +1 -0
- winipedia_utils-0.1.17/winipedia_utils/resources/svgs/download_arrow.svg +3 -0
- winipedia_utils-0.1.17/winipedia_utils/resources/svgs/exit_fullscreen_icon.svg +7 -0
- winipedia_utils-0.1.17/winipedia_utils/resources/svgs/fullscreen_icon.svg +4 -0
- winipedia_utils-0.1.17/winipedia_utils/resources/svgs/pause_icon.svg +4 -0
- winipedia_utils-0.1.17/winipedia_utils/resources/svgs/play_icon.svg +8 -0
- winipedia_utils-0.1.17/winipedia_utils/resources/svgs/svg.py +11 -0
- winipedia_utils-0.1.17/winipedia_utils/security/__init__.py +1 -0
- winipedia_utils-0.1.17/winipedia_utils/security/keyring.py +30 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/LICENSE +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/README.md +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/__init__.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/concurrent/__init__.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/concurrent/concurrent.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/concurrent/multiprocessing.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/concurrent/multithreading.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/consts.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/data/__init__.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/data/dataframe.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/django/__init__.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/django/bulk.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/django/command.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/django/database.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/git/__init__.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/git/gitignore/__init__.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/git/gitignore/gitignore.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/git/pre_commit/__init__.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/git/pre_commit/config.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/git/pre_commit/hooks.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/git/pre_commit/run_hooks.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/iterating/__init__.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/iterating/iterate.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/logging/__init__.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/logging/ansi.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/logging/config.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/logging/logger.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/modules/__init__.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/modules/class_.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/modules/function.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/modules/package.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/oop/__init__.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/oop/mixins/__init__.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/oop/mixins/meta.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/oop/mixins/mixin.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/os/__init__.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/os/os.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/projects/__init__.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/projects/poetry/__init__.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/projects/poetry/poetry.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/projects/project.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/py.typed +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/setup.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/testing/__init__.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/testing/assertions.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/testing/convention.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/testing/create_tests.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/testing/fixtures.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/testing/tests/__init__.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/testing/tests/base/__init__.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/testing/tests/base/fixtures/__init__.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/testing/tests/base/fixtures/fixture.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/testing/tests/base/fixtures/scopes/__init__.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/testing/tests/base/fixtures/scopes/class_.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/testing/tests/base/fixtures/scopes/function.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/testing/tests/base/fixtures/scopes/module.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/testing/tests/base/fixtures/scopes/package.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/testing/tests/base/fixtures/scopes/session.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/testing/tests/base/utils/__init__.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/testing/tests/base/utils/utils.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/testing/tests/conftest.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/text/__init__.py +0 -0
- {winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/text/string.py +0 -0
@@ -1,18 +1,22 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: winipedia-utils
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.17
|
4
4
|
Summary: A package with many utility functions
|
5
5
|
License: MIT
|
6
6
|
Author: Winipedia
|
7
7
|
Author-email: win.steveker@gmx.de
|
8
|
-
Requires-Python: >=3.12
|
8
|
+
Requires-Python: >=3.12,<3.14
|
9
9
|
Classifier: License :: OSI Approved :: MIT License
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
11
11
|
Classifier: Programming Language :: Python :: 3.12
|
12
12
|
Classifier: Programming Language :: Python :: 3.13
|
13
|
+
Requires-Dist: cryptography (>=45.0.5,<46.0.0)
|
13
14
|
Requires-Dist: defusedxml (>=0.7.1,<0.8.0)
|
14
15
|
Requires-Dist: django (>=5.2.1,<6.0.0)
|
16
|
+
Requires-Dist: keyring (>=25.6.0,<26.0.0)
|
15
17
|
Requires-Dist: pathspec (>=0.12.1,<0.13.0)
|
18
|
+
Requires-Dist: pyqt-toast-notification (>=1.3.3,<2.0.0)
|
19
|
+
Requires-Dist: pyside6 (>=6.9.1,<7.0.0)
|
16
20
|
Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
|
17
21
|
Requires-Dist: setuptools (>=80.3.1,<81.0.0)
|
18
22
|
Requires-Dist: tomlkit (>=0.13.2,<0.14.0)
|
@@ -1,10 +1,10 @@
|
|
1
1
|
# Project section
|
2
2
|
[project]
|
3
3
|
name = "winipedia-utils"
|
4
|
-
version = "0.1.
|
4
|
+
version = "0.1.17"
|
5
5
|
description = "A package with many utility functions"
|
6
6
|
readme = "README.md"
|
7
|
-
requires-python = ">=3.12"
|
7
|
+
requires-python = ">=3.12,<3.14"
|
8
8
|
dynamic = [ "dependencies",]
|
9
9
|
authors = [
|
10
10
|
{ name = "Winipedia", email = "win.steveker@gmx.de"}
|
@@ -28,6 +28,10 @@ tomlkit = "^0.13.2"
|
|
28
28
|
pathspec = "^0.12.1"
|
29
29
|
django = "^5.2.1"
|
30
30
|
pyyaml = "^6.0.2"
|
31
|
+
keyring = "^25.6.0"
|
32
|
+
cryptography = "^45.0.5"
|
33
|
+
pyside6 = "^6.9.1"
|
34
|
+
pyqt-toast-notification = "^1.3.3"
|
31
35
|
|
32
36
|
[tool.poetry.group.dev.dependencies]
|
33
37
|
ruff = "^0.11.7"
|
@@ -43,7 +47,7 @@ pytest-mock = "^3.14.0"
|
|
43
47
|
django-stubs = "^5.2.0"
|
44
48
|
|
45
49
|
[tool.ruff]
|
46
|
-
exclude = [ ".*",]
|
50
|
+
exclude = [ ".*", "**/migrations/*.py",]
|
47
51
|
[tool.ruff.lint]
|
48
52
|
select = [ "ALL",]
|
49
53
|
ignore = [ "D203", "D213", "COM812", "ANN401",]
|
@@ -11,6 +11,7 @@ making them suitable for code generation, testing frameworks, and dynamic import
|
|
11
11
|
|
12
12
|
import inspect
|
13
13
|
import os
|
14
|
+
import sys
|
14
15
|
import time
|
15
16
|
from collections.abc import Callable, Sequence
|
16
17
|
from importlib import import_module
|
@@ -115,6 +116,9 @@ def to_path(module_name: str | ModuleType | Path, *, is_package: bool) -> Path:
|
|
115
116
|
"""
|
116
117
|
module_name = to_module_name(module_name)
|
117
118
|
path = Path(module_name.replace(".", os.sep))
|
119
|
+
# for smth like pyinstaller we support frozen path
|
120
|
+
if getattr(sys, "frozen", False):
|
121
|
+
path = Path(getattr(sys, "_MEIPASS", "")) / path
|
118
122
|
if is_package:
|
119
123
|
return path
|
120
124
|
return path.with_suffix(".py")
|
@@ -32,7 +32,7 @@ def _get_pyproject_toml_tool_configs() -> dict[str, Any]:
|
|
32
32
|
"""Get the tool configurations for pyproject.toml."""
|
33
33
|
return {
|
34
34
|
"ruff": {
|
35
|
-
"exclude": [".*"],
|
35
|
+
"exclude": [".*", "**/migrations/*.py"],
|
36
36
|
"lint": {
|
37
37
|
"select": ["ALL"],
|
38
38
|
"ignore": ["D203", "D213", "COM812", "ANN401"],
|
@@ -0,0 +1 @@
|
|
1
|
+
"""__init__ module for winipedia_utils.pyside6."""
|
@@ -0,0 +1 @@
|
|
1
|
+
"""__init__ module for winipedia_utils.pyside6.ui."""
|
@@ -0,0 +1 @@
|
|
1
|
+
"""__init__ module."""
|
@@ -0,0 +1,117 @@
|
|
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, final
|
9
|
+
|
10
|
+
from PySide6.QtCore import QObject
|
11
|
+
from PySide6.QtGui import QIcon
|
12
|
+
from PySide6.QtWidgets import QStackedWidget
|
13
|
+
|
14
|
+
from winipedia_utils.modules.package import walk_package
|
15
|
+
from winipedia_utils.oop.mixins.meta import ABCImplementationLoggingMeta
|
16
|
+
from winipedia_utils.resources.svgs.svg import get_svg_path
|
17
|
+
from winipedia_utils.text.string import split_on_uppercase
|
18
|
+
|
19
|
+
# Avoid circular import
|
20
|
+
if TYPE_CHECKING:
|
21
|
+
from winipedia_utils.pyside.ui.pages.base.base import Base as BasePage
|
22
|
+
|
23
|
+
|
24
|
+
class QABCImplementationLoggingMeta(
|
25
|
+
ABCImplementationLoggingMeta,
|
26
|
+
type(QObject), # type: ignore[misc]
|
27
|
+
):
|
28
|
+
"""Metaclass for the QABCImplementationLoggingMixin."""
|
29
|
+
|
30
|
+
|
31
|
+
class Base(metaclass=QABCImplementationLoggingMeta):
|
32
|
+
"""Base UI class for a Qt application."""
|
33
|
+
|
34
|
+
@final
|
35
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
36
|
+
"""Initialize the base UI."""
|
37
|
+
super().__init__(*args, **kwargs)
|
38
|
+
self.base_setup()
|
39
|
+
self.pre_setup()
|
40
|
+
self.setup()
|
41
|
+
self.post_setup()
|
42
|
+
|
43
|
+
@abstractmethod
|
44
|
+
def base_setup(self) -> None:
|
45
|
+
"""Get the Qt object of the UI."""
|
46
|
+
|
47
|
+
@abstractmethod
|
48
|
+
def setup(self) -> None:
|
49
|
+
"""Setup the UI."""
|
50
|
+
|
51
|
+
@abstractmethod
|
52
|
+
def pre_setup(self) -> None:
|
53
|
+
"""Setup the UI."""
|
54
|
+
|
55
|
+
@abstractmethod
|
56
|
+
def post_setup(self) -> None:
|
57
|
+
"""Setup the UI."""
|
58
|
+
|
59
|
+
@classmethod
|
60
|
+
@final
|
61
|
+
def get_display_name(cls) -> str:
|
62
|
+
"""Get the display name of the UI."""
|
63
|
+
return " ".join(split_on_uppercase(cls.__name__))
|
64
|
+
|
65
|
+
@classmethod
|
66
|
+
@final
|
67
|
+
def get_subclasses(cls, package: ModuleType) -> list[type[Self]]:
|
68
|
+
"""Get all subclasses of the UI.
|
69
|
+
|
70
|
+
Args:
|
71
|
+
package: The package to search for subclasses in.
|
72
|
+
"""
|
73
|
+
_ = list(walk_package(package))
|
74
|
+
|
75
|
+
children = cls.__subclasses__()
|
76
|
+
return sorted(children, key=lambda cls: cls.__name__)
|
77
|
+
|
78
|
+
@final
|
79
|
+
def set_current_page(self, page_cls: type["BasePage"]) -> None:
|
80
|
+
"""Set the current page."""
|
81
|
+
self.get_stack().setCurrentWidget(self.get_page(page_cls))
|
82
|
+
|
83
|
+
@final
|
84
|
+
def get_stack(self) -> QStackedWidget:
|
85
|
+
"""Get the stack of the window."""
|
86
|
+
from winipedia_utils.pyside.ui.windows.base.base import Base as BaseWindow
|
87
|
+
|
88
|
+
window = getattr(self, "window", lambda: None)()
|
89
|
+
|
90
|
+
if not isinstance(window, BaseWindow):
|
91
|
+
msg = f"Cannot get stack on {window.__class__.__name__}"
|
92
|
+
raise TypeError(msg)
|
93
|
+
|
94
|
+
return window.stack
|
95
|
+
|
96
|
+
@final
|
97
|
+
def get_stack_pages(self) -> list["BasePage"]:
|
98
|
+
"""Get all the pages."""
|
99
|
+
# Import here to avoid circular import
|
100
|
+
|
101
|
+
stack = self.get_stack()
|
102
|
+
# get all the pages
|
103
|
+
return [cast("BasePage", stack.widget(i)) for i in range(stack.count())]
|
104
|
+
|
105
|
+
@final
|
106
|
+
def get_page[T: "BasePage"](self, page_cls: type[T]) -> T:
|
107
|
+
"""Get the page."""
|
108
|
+
page = next(
|
109
|
+
page for page in self.get_stack_pages() if page.__class__ is page_cls
|
110
|
+
)
|
111
|
+
return cast("T", page)
|
112
|
+
|
113
|
+
@classmethod
|
114
|
+
@final
|
115
|
+
def get_svg_icon(cls, svg_name: str) -> QIcon:
|
116
|
+
"""Get the Qicon for a svg."""
|
117
|
+
return QIcon(str(get_svg_path(svg_name)))
|
@@ -0,0 +1 @@
|
|
1
|
+
"""__init__ module."""
|
@@ -0,0 +1 @@
|
|
1
|
+
"""__init__ module."""
|
@@ -0,0 +1,78 @@
|
|
1
|
+
"""Base page module.
|
2
|
+
|
3
|
+
This module contains the base page class for the VideoVault application.
|
4
|
+
"""
|
5
|
+
|
6
|
+
from functools import partial
|
7
|
+
from types import ModuleType
|
8
|
+
from typing import final
|
9
|
+
|
10
|
+
from PySide6.QtCore import Qt
|
11
|
+
from PySide6.QtWidgets import (
|
12
|
+
QHBoxLayout,
|
13
|
+
QLayout,
|
14
|
+
QMenu,
|
15
|
+
QPushButton,
|
16
|
+
QSizePolicy,
|
17
|
+
QVBoxLayout,
|
18
|
+
QWidget,
|
19
|
+
)
|
20
|
+
|
21
|
+
from winipedia_utils.pyside.ui.base.base import Base as BaseUI
|
22
|
+
|
23
|
+
|
24
|
+
class Base(BaseUI, QWidget):
|
25
|
+
"""Base page class for the VideoVault application."""
|
26
|
+
|
27
|
+
PAGES_PACKAGE: ModuleType = NotImplemented
|
28
|
+
|
29
|
+
@final
|
30
|
+
def base_setup(self) -> None:
|
31
|
+
"""Get the Qt object of the UI."""
|
32
|
+
self.v_layout = QVBoxLayout()
|
33
|
+
self.setLayout(self.v_layout)
|
34
|
+
|
35
|
+
# add a horizontal layout for the top row
|
36
|
+
self.h_layout = QHBoxLayout()
|
37
|
+
self.v_layout.addLayout(self.h_layout)
|
38
|
+
|
39
|
+
self.add_menu_dropdown_button()
|
40
|
+
|
41
|
+
@final
|
42
|
+
def add_menu_dropdown_button(self) -> None:
|
43
|
+
"""Add a dropdown menu that leadds to each page."""
|
44
|
+
self.menu_button = QPushButton("Menu")
|
45
|
+
self.menu_button.setSizePolicy(
|
46
|
+
QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum
|
47
|
+
)
|
48
|
+
self.h_layout.addWidget(
|
49
|
+
self.menu_button,
|
50
|
+
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
|
51
|
+
)
|
52
|
+
self.menu_dropdown = QMenu(self.menu_button)
|
53
|
+
self.menu_button.setMenu(self.menu_dropdown)
|
54
|
+
|
55
|
+
for page_cls in self.get_page_classes():
|
56
|
+
action = self.menu_dropdown.addAction(page_cls.get_display_name())
|
57
|
+
action.triggered.connect(partial(self.set_current_page, page_cls))
|
58
|
+
|
59
|
+
@final
|
60
|
+
def add_to_page_button(
|
61
|
+
self, to_page_cls: type["Base"], layout: QLayout
|
62
|
+
) -> QPushButton:
|
63
|
+
"""Add a button to go to the page."""
|
64
|
+
button = QPushButton(to_page_cls.get_display_name())
|
65
|
+
|
66
|
+
# connect to open page on click
|
67
|
+
button.clicked.connect(lambda: self.set_current_page(to_page_cls))
|
68
|
+
|
69
|
+
# add to layout
|
70
|
+
layout.addWidget(button)
|
71
|
+
|
72
|
+
return button
|
73
|
+
|
74
|
+
@classmethod
|
75
|
+
@final
|
76
|
+
def get_page_classes(cls) -> list[type["Base"]]:
|
77
|
+
"""Get all the page classes."""
|
78
|
+
return Base.get_subclasses(cls.PAGES_PACKAGE)
|
@@ -0,0 +1,24 @@
|
|
1
|
+
"""Add downloads page module.
|
2
|
+
|
3
|
+
This module contains the add downloads page class for the VideoVault application.
|
4
|
+
"""
|
5
|
+
|
6
|
+
from typing import final
|
7
|
+
|
8
|
+
from winipedia_utils.pyside.ui.pages.base.base import Base as BasePage
|
9
|
+
from winipedia_utils.pyside.ui.widgets.browser import Browser
|
10
|
+
|
11
|
+
|
12
|
+
class AddDownloads(BasePage):
|
13
|
+
"""Add downloads page for the VideoVault application."""
|
14
|
+
|
15
|
+
@final
|
16
|
+
def setup(self) -> None:
|
17
|
+
"""Setup the UI."""
|
18
|
+
# add a download button in the top right
|
19
|
+
self.add_brwoser()
|
20
|
+
|
21
|
+
@final
|
22
|
+
def add_brwoser(self) -> None:
|
23
|
+
"""Add a browser to surfe the web."""
|
24
|
+
self.browser = Browser(self.v_layout)
|
@@ -0,0 +1,26 @@
|
|
1
|
+
"""Player page module.
|
2
|
+
|
3
|
+
This module contains the player page class for the VideoVault application.
|
4
|
+
"""
|
5
|
+
|
6
|
+
from typing import final
|
7
|
+
|
8
|
+
from winipedia_utils.pyside.ui.pages.base.base import Base as BasePage
|
9
|
+
from winipedia_utils.pyside.ui.widgets.media_player import MediaPlayer
|
10
|
+
|
11
|
+
|
12
|
+
class Player(BasePage):
|
13
|
+
"""Player page for the VideoVault application."""
|
14
|
+
|
15
|
+
@final
|
16
|
+
def setup(self) -> None:
|
17
|
+
"""Setup the UI."""
|
18
|
+
self.media_player = MediaPlayer(self.v_layout)
|
19
|
+
|
20
|
+
@final
|
21
|
+
def play_data(self, data: bytes, name: str) -> None:
|
22
|
+
"""Play the video."""
|
23
|
+
# set current page to player
|
24
|
+
self.set_current_page(self.__class__)
|
25
|
+
# Stop current playback and clean up resources
|
26
|
+
self.media_player.play_data(data, name)
|
@@ -0,0 +1 @@
|
|
1
|
+
"""__init__ module."""
|
@@ -0,0 +1,166 @@
|
|
1
|
+
"""Browser module.
|
2
|
+
|
3
|
+
This module contains the browser class for the application.
|
4
|
+
"""
|
5
|
+
|
6
|
+
from collections import defaultdict
|
7
|
+
from http.cookiejar import Cookie
|
8
|
+
from typing import Any
|
9
|
+
|
10
|
+
from PySide6.QtCore import QUrl
|
11
|
+
from PySide6.QtGui import QIcon
|
12
|
+
from PySide6.QtNetwork import QNetworkCookie
|
13
|
+
from PySide6.QtWebEngineWidgets import QWebEngineView
|
14
|
+
from PySide6.QtWidgets import (
|
15
|
+
QHBoxLayout,
|
16
|
+
QLayout,
|
17
|
+
QLineEdit,
|
18
|
+
QPushButton,
|
19
|
+
QSizePolicy,
|
20
|
+
QVBoxLayout,
|
21
|
+
QWidget,
|
22
|
+
)
|
23
|
+
|
24
|
+
|
25
|
+
class Browser(QWebEngineView):
|
26
|
+
"""Browser class that creates a simple ready to use browser and not just a view."""
|
27
|
+
|
28
|
+
def __init__(self, parent_layout: QLayout, *args: Any, **kwargs: Any) -> None:
|
29
|
+
"""Initialize the browser."""
|
30
|
+
super().__init__(*args, **kwargs)
|
31
|
+
self.parent_layout = parent_layout
|
32
|
+
self.make_widget()
|
33
|
+
self.connect_signals()
|
34
|
+
self.load_first_url()
|
35
|
+
|
36
|
+
def make_address_bar(self) -> None:
|
37
|
+
"""Make the address bar."""
|
38
|
+
self.address_bar_layout = QHBoxLayout()
|
39
|
+
|
40
|
+
# Add back button
|
41
|
+
self.back_button = QPushButton()
|
42
|
+
self.back_button.setIcon(QIcon.fromTheme("go-previous"))
|
43
|
+
self.back_button.setToolTip("Go back")
|
44
|
+
self.back_button.clicked.connect(self.back)
|
45
|
+
self.address_bar_layout.addWidget(self.back_button)
|
46
|
+
|
47
|
+
# Add forward button
|
48
|
+
self.forward_button = QPushButton()
|
49
|
+
self.forward_button.setIcon(QIcon.fromTheme("go-next"))
|
50
|
+
self.forward_button.setToolTip("Go forward")
|
51
|
+
self.forward_button.clicked.connect(self.forward)
|
52
|
+
self.address_bar_layout.addWidget(self.forward_button)
|
53
|
+
|
54
|
+
# Add address bar
|
55
|
+
self.address_bar = QLineEdit()
|
56
|
+
self.address_bar.setPlaceholderText("Enter URL...")
|
57
|
+
self.address_bar.returnPressed.connect(self.navigate_to_url)
|
58
|
+
self.address_bar_layout.addWidget(self.address_bar)
|
59
|
+
|
60
|
+
# Add go button
|
61
|
+
self.go_button = QPushButton("Go")
|
62
|
+
self.go_button.clicked.connect(self.navigate_to_url)
|
63
|
+
self.address_bar_layout.addWidget(self.go_button)
|
64
|
+
|
65
|
+
self.browser_layout.addLayout(self.address_bar_layout)
|
66
|
+
|
67
|
+
def navigate_to_url(self) -> None:
|
68
|
+
"""Navigate to the URL entered in the address bar."""
|
69
|
+
url = self.address_bar.text()
|
70
|
+
self.load(QUrl(url))
|
71
|
+
|
72
|
+
def make_widget(self) -> None:
|
73
|
+
"""Make the widget."""
|
74
|
+
self.browser_widget = QWidget()
|
75
|
+
self.browser_layout = QVBoxLayout(self.browser_widget)
|
76
|
+
self.set_size_policy()
|
77
|
+
self.make_address_bar()
|
78
|
+
self.browser_layout.addWidget(self)
|
79
|
+
self.parent_layout.addWidget(self.browser_widget)
|
80
|
+
|
81
|
+
def set_size_policy(self) -> None:
|
82
|
+
"""Set the size policy."""
|
83
|
+
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
84
|
+
|
85
|
+
def connect_signals(self) -> None:
|
86
|
+
"""Connect the signals."""
|
87
|
+
self.connect_load_finished_signal()
|
88
|
+
self.connect_on_cookie_added_signal()
|
89
|
+
|
90
|
+
def connect_load_finished_signal(self) -> None:
|
91
|
+
"""Connect the load finished signal."""
|
92
|
+
self.loadFinished.connect(self.on_load_finished)
|
93
|
+
|
94
|
+
def on_load_finished(self, _ok: bool) -> None: # noqa: FBT001
|
95
|
+
"""Handle the load finished signal."""
|
96
|
+
self.update_address_bar(self.url())
|
97
|
+
|
98
|
+
def update_address_bar(self, url: QUrl) -> None:
|
99
|
+
"""Update the address bar with the current URL."""
|
100
|
+
self.address_bar.setText(url.toString())
|
101
|
+
|
102
|
+
def connect_on_cookie_added_signal(self) -> None:
|
103
|
+
"""Connect the on cookie added signal."""
|
104
|
+
self.cookies: dict[str, list[QNetworkCookie]] = defaultdict(list)
|
105
|
+
self.page().profile().cookieStore().cookieAdded.connect(self.on_cookie_added)
|
106
|
+
|
107
|
+
def on_cookie_added(self, cookie: Any) -> None:
|
108
|
+
"""Handle the on cookie added signal."""
|
109
|
+
self.cookies[cookie.domain()].append(cookie)
|
110
|
+
|
111
|
+
def load_first_url(self) -> None:
|
112
|
+
"""Load the first URL."""
|
113
|
+
self.load(QUrl("https://www.google.com/"))
|
114
|
+
|
115
|
+
@property
|
116
|
+
def http_cookies(self) -> dict[str, list[Cookie]]:
|
117
|
+
"""Get the http cookies for the given URL."""
|
118
|
+
return {
|
119
|
+
domain: self.qcookies_to_httpcookies(qcookies)
|
120
|
+
for domain, qcookies in self.cookies.items()
|
121
|
+
}
|
122
|
+
|
123
|
+
def qcookies_to_httpcookies(self, qcookies: list[QNetworkCookie]) -> list[Cookie]:
|
124
|
+
"""Convert a list of QNetworkCookies to a CookieJar."""
|
125
|
+
return [self.qcookie_to_httpcookie(q_cookie) for q_cookie in qcookies]
|
126
|
+
|
127
|
+
def qcookie_to_httpcookie(self, qcookie: QNetworkCookie) -> Cookie:
|
128
|
+
"""Convert a QNetworkCookie to a http.cookiejar.Cookie."""
|
129
|
+
name = bytes(qcookie.name().data()).decode()
|
130
|
+
value = bytes(qcookie.value().data()).decode()
|
131
|
+
domain = qcookie.domain()
|
132
|
+
path = qcookie.path() if qcookie.path() else "/"
|
133
|
+
secure = qcookie.isSecure()
|
134
|
+
expires = None
|
135
|
+
if qcookie.expirationDate().isValid():
|
136
|
+
expires = int(qcookie.expirationDate().toSecsSinceEpoch())
|
137
|
+
rest = {"HttpOnly": str(qcookie.isHttpOnly())}
|
138
|
+
|
139
|
+
return Cookie(
|
140
|
+
version=0,
|
141
|
+
name=name,
|
142
|
+
value=value,
|
143
|
+
port=None,
|
144
|
+
port_specified=False,
|
145
|
+
domain=domain,
|
146
|
+
domain_specified=bool(domain),
|
147
|
+
domain_initial_dot=domain.startswith("."),
|
148
|
+
path=path,
|
149
|
+
path_specified=bool(path),
|
150
|
+
secure=secure,
|
151
|
+
expires=expires or None,
|
152
|
+
discard=False,
|
153
|
+
comment=None,
|
154
|
+
comment_url=None,
|
155
|
+
rest=rest,
|
156
|
+
rfc2109=False,
|
157
|
+
)
|
158
|
+
|
159
|
+
def get_domain_cookies(self, domain: str) -> list[QNetworkCookie]:
|
160
|
+
"""Get the cookies for the given domain."""
|
161
|
+
return self.cookies[domain]
|
162
|
+
|
163
|
+
def get_domain_http_cookies(self, domain: str) -> list[Cookie]:
|
164
|
+
"""Get the http cookies for the given domain."""
|
165
|
+
cookies = self.get_domain_cookies(domain)
|
166
|
+
return self.qcookies_to_httpcookies(cookies)
|
@@ -0,0 +1,265 @@
|
|
1
|
+
"""Media player module.
|
2
|
+
|
3
|
+
This module contains the media player class.
|
4
|
+
"""
|
5
|
+
|
6
|
+
from functools import partial
|
7
|
+
from typing import Any
|
8
|
+
|
9
|
+
from PySide6.QtCore import QBuffer, QByteArray, Qt, Signal
|
10
|
+
from PySide6.QtMultimedia import QAudioOutput, QMediaPlayer
|
11
|
+
from PySide6.QtMultimediaWidgets import QVideoWidget
|
12
|
+
from PySide6.QtWidgets import (
|
13
|
+
QHBoxLayout,
|
14
|
+
QLayout,
|
15
|
+
QMenu,
|
16
|
+
QPushButton,
|
17
|
+
QSizePolicy,
|
18
|
+
QSlider,
|
19
|
+
QVBoxLayout,
|
20
|
+
QWidget,
|
21
|
+
)
|
22
|
+
|
23
|
+
from winipedia_utils.pyside.ui.base.base import Base as BaseUI
|
24
|
+
|
25
|
+
|
26
|
+
class ClickableWidget(QWidget):
|
27
|
+
"""Widget that can be clicked."""
|
28
|
+
|
29
|
+
clicked = Signal()
|
30
|
+
|
31
|
+
def mousePressEvent(self, event: Any) -> None: # noqa: N802
|
32
|
+
"""Handle mouse press event."""
|
33
|
+
if event.button() == Qt.MouseButton.LeftButton:
|
34
|
+
self.clicked.emit()
|
35
|
+
super().mousePressEvent(event)
|
36
|
+
|
37
|
+
|
38
|
+
class ClickableVideoWidget(QVideoWidget):
|
39
|
+
"""Video widget that can be clicked."""
|
40
|
+
|
41
|
+
clicked = Signal()
|
42
|
+
|
43
|
+
def mousePressEvent(self, event: Any) -> None: # noqa: N802
|
44
|
+
"""Handle mouse press event."""
|
45
|
+
if event.button() == Qt.MouseButton.LeftButton:
|
46
|
+
self.clicked.emit()
|
47
|
+
super().mousePressEvent(event)
|
48
|
+
|
49
|
+
|
50
|
+
class MediaPlayer(QMediaPlayer):
|
51
|
+
"""Media player class."""
|
52
|
+
|
53
|
+
def __init__(self, parent_layout: QLayout, *args: Any, **kwargs: Any) -> None:
|
54
|
+
"""Initialize the media player."""
|
55
|
+
super().__init__(*args, **kwargs)
|
56
|
+
self.parent_layout = parent_layout
|
57
|
+
self.make_widget()
|
58
|
+
|
59
|
+
def make_widget(self) -> None:
|
60
|
+
"""Make the widget."""
|
61
|
+
self.media_player_widget = QWidget()
|
62
|
+
self.media_player_layout = QVBoxLayout(self.media_player_widget)
|
63
|
+
self.parent_layout.addWidget(self.media_player_widget)
|
64
|
+
self.add_media_controls_above()
|
65
|
+
self.make_video_widget()
|
66
|
+
self.add_media_controls_below()
|
67
|
+
|
68
|
+
def make_video_widget(self) -> None:
|
69
|
+
"""Make the video widget."""
|
70
|
+
self.video_widget = ClickableVideoWidget()
|
71
|
+
self.video_widget.clicked.connect(self.on_video_clicked)
|
72
|
+
self.video_widget.setSizePolicy(
|
73
|
+
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
|
74
|
+
)
|
75
|
+
self.setVideoOutput(self.video_widget)
|
76
|
+
|
77
|
+
self.audio_output = QAudioOutput()
|
78
|
+
self.setAudioOutput(self.audio_output)
|
79
|
+
|
80
|
+
self.media_player_layout.addWidget(self.video_widget)
|
81
|
+
|
82
|
+
def on_video_clicked(self) -> None:
|
83
|
+
"""Handle video widget click."""
|
84
|
+
if self.media_controls_widget_above.isVisible():
|
85
|
+
self.hide_media_controls()
|
86
|
+
return
|
87
|
+
self.show_media_controls()
|
88
|
+
|
89
|
+
def show_media_controls(self) -> None:
|
90
|
+
"""Show media controls."""
|
91
|
+
self.media_controls_widget_above.show()
|
92
|
+
self.media_controls_widget_below.show()
|
93
|
+
|
94
|
+
def hide_media_controls(self) -> None:
|
95
|
+
"""Hide media controls."""
|
96
|
+
self.media_controls_widget_above.hide()
|
97
|
+
self.media_controls_widget_below.hide()
|
98
|
+
|
99
|
+
def add_media_controls_above(self) -> None:
|
100
|
+
"""Add media controls above the video."""
|
101
|
+
# main above widget
|
102
|
+
self.media_controls_widget_above = QWidget()
|
103
|
+
self.media_controls_layout_above = QHBoxLayout(self.media_controls_widget_above)
|
104
|
+
self.media_player_layout.addWidget(self.media_controls_widget_above)
|
105
|
+
# left contorls
|
106
|
+
self.left_controls_widget = QWidget()
|
107
|
+
self.left_controls_layout = QHBoxLayout(self.left_controls_widget)
|
108
|
+
self.media_controls_layout_above.addWidget(
|
109
|
+
self.left_controls_widget, alignment=Qt.AlignmentFlag.AlignLeft
|
110
|
+
)
|
111
|
+
# center contorls
|
112
|
+
self.center_controls_widget = QWidget()
|
113
|
+
self.center_controls_layout = QHBoxLayout(self.center_controls_widget)
|
114
|
+
self.media_controls_layout_above.addWidget(
|
115
|
+
self.center_controls_widget, alignment=Qt.AlignmentFlag.AlignCenter
|
116
|
+
)
|
117
|
+
self.right_controls_widget = QWidget()
|
118
|
+
self.right_controls_layout = QHBoxLayout(self.right_controls_widget)
|
119
|
+
self.media_controls_layout_above.addWidget(
|
120
|
+
self.right_controls_widget, alignment=Qt.AlignmentFlag.AlignRight
|
121
|
+
)
|
122
|
+
|
123
|
+
self.add_speed_control()
|
124
|
+
self.add_volume_control()
|
125
|
+
self.add_playback_control()
|
126
|
+
self.add_fullscreen_control()
|
127
|
+
|
128
|
+
def add_media_controls_below(self) -> None:
|
129
|
+
"""Add media controls below the video."""
|
130
|
+
self.media_controls_widget_below = QWidget()
|
131
|
+
self.media_controls_layout_below = QHBoxLayout(self.media_controls_widget_below)
|
132
|
+
self.media_player_layout.addWidget(self.media_controls_widget_below)
|
133
|
+
self.add_progress_control()
|
134
|
+
|
135
|
+
def add_playback_control(self) -> None:
|
136
|
+
"""Add playback control."""
|
137
|
+
self.play_icon = BaseUI.get_svg_icon("play_icon.svg")
|
138
|
+
self.pause_icon = BaseUI.get_svg_icon("pause_icon.svg")
|
139
|
+
# Pause symbol: ⏸ (U+23F8)
|
140
|
+
self.playback_button = QPushButton()
|
141
|
+
self.playback_button.setIcon(self.pause_icon)
|
142
|
+
self.playback_button.clicked.connect(self.toggle_playback)
|
143
|
+
|
144
|
+
self.center_controls_layout.addWidget(self.playback_button)
|
145
|
+
|
146
|
+
def toggle_playback(self) -> None:
|
147
|
+
"""Toggle playback."""
|
148
|
+
if self.playbackState() == QMediaPlayer.PlaybackState.PlayingState:
|
149
|
+
self.pause()
|
150
|
+
self.playback_button.setIcon(self.play_icon)
|
151
|
+
else:
|
152
|
+
self.play()
|
153
|
+
self.playback_button.setIcon(self.pause_icon)
|
154
|
+
|
155
|
+
def add_speed_control(self) -> None:
|
156
|
+
"""Add speed control.
|
157
|
+
|
158
|
+
A button in the top left that on click shows a dropdown to select the speed.
|
159
|
+
"""
|
160
|
+
self.default_speed = 1
|
161
|
+
self.speed_options = [0.2, 0.5, self.default_speed, 1.5, 2, 3, 4, 5]
|
162
|
+
self.speed_button = QPushButton(f"{self.default_speed}x")
|
163
|
+
self.speed_menu = QMenu(self.speed_button)
|
164
|
+
for speed in self.speed_options:
|
165
|
+
action = self.speed_menu.addAction(f"{speed}x")
|
166
|
+
action.triggered.connect(partial(self.change_speed, speed))
|
167
|
+
|
168
|
+
self.speed_button.setMenu(self.speed_menu)
|
169
|
+
self.left_controls_layout.addWidget(self.speed_button)
|
170
|
+
|
171
|
+
def change_speed(self, speed: float) -> None:
|
172
|
+
"""Change playback speed."""
|
173
|
+
self.setPlaybackRate(speed)
|
174
|
+
self.speed_button.setText(f"{speed}x")
|
175
|
+
|
176
|
+
def add_volume_control(self) -> None:
|
177
|
+
"""Add volume control."""
|
178
|
+
self.volume_slider = QSlider(Qt.Orientation.Horizontal)
|
179
|
+
self.volume_slider.setRange(0, 100)
|
180
|
+
self.volume_slider.valueChanged.connect(self.on_volume_changed)
|
181
|
+
self.left_controls_layout.addWidget(self.volume_slider)
|
182
|
+
|
183
|
+
def on_volume_changed(self, value: int) -> None:
|
184
|
+
"""Handle volume slider value change."""
|
185
|
+
volume = value / 100.0 # Convert to 0.0-1.0 range
|
186
|
+
self.audio_output.setVolume(volume)
|
187
|
+
|
188
|
+
def add_fullscreen_control(self) -> None:
|
189
|
+
"""Add fullscreen control."""
|
190
|
+
self.fullscreen_icon = BaseUI.get_svg_icon("fullscreen_icon.svg")
|
191
|
+
self.exit_fullscreen_icon = BaseUI.get_svg_icon("exit_fullscreen_icon.svg")
|
192
|
+
self.fullscreen_button = QPushButton()
|
193
|
+
self.fullscreen_button.setIcon(self.fullscreen_icon)
|
194
|
+
|
195
|
+
self.parent_widget = self.parent_layout.parentWidget()
|
196
|
+
self.other_visible_widgets = [
|
197
|
+
w
|
198
|
+
for w in set(self.parent_widget.findChildren(QWidget))
|
199
|
+
- {
|
200
|
+
self.media_player_widget,
|
201
|
+
*self.media_player_widget.findChildren(QWidget),
|
202
|
+
}
|
203
|
+
if w.isVisible() or not (w.isHidden() or w.isVisible())
|
204
|
+
]
|
205
|
+
self.fullscreen_button.clicked.connect(self.toggle_fullscreen)
|
206
|
+
|
207
|
+
self.right_controls_layout.addWidget(self.fullscreen_button)
|
208
|
+
|
209
|
+
def toggle_fullscreen(self) -> None:
|
210
|
+
"""Toggle fullscreen mode."""
|
211
|
+
# Get the main window
|
212
|
+
main_window = self.media_player_widget.window()
|
213
|
+
if main_window.isFullScreen():
|
214
|
+
for widget in self.other_visible_widgets:
|
215
|
+
widget.show()
|
216
|
+
# show the window in the previous size
|
217
|
+
main_window.showMaximized()
|
218
|
+
self.fullscreen_button.setIcon(self.fullscreen_icon)
|
219
|
+
else:
|
220
|
+
for widget in self.other_visible_widgets:
|
221
|
+
widget.hide()
|
222
|
+
main_window.showFullScreen()
|
223
|
+
self.fullscreen_button.setIcon(self.exit_fullscreen_icon)
|
224
|
+
|
225
|
+
def add_progress_control(self) -> None:
|
226
|
+
"""Add progress control."""
|
227
|
+
self.progress_slider = QSlider(Qt.Orientation.Horizontal)
|
228
|
+
self.media_controls_layout_below.addWidget(self.progress_slider)
|
229
|
+
|
230
|
+
# Connect media player signals to update the progress slider
|
231
|
+
self.positionChanged.connect(self.update_position)
|
232
|
+
self.durationChanged.connect(self.set_duration)
|
233
|
+
|
234
|
+
# Connect slider signals to update video position
|
235
|
+
self.progress_slider.sliderMoved.connect(self.set_position)
|
236
|
+
self.progress_slider.sliderReleased.connect(self.slider_released)
|
237
|
+
|
238
|
+
def update_position(self, position: int) -> None:
|
239
|
+
"""Update the progress slider position."""
|
240
|
+
# Only update if not being dragged to prevent jumps during manual sliding
|
241
|
+
if not self.progress_slider.isSliderDown():
|
242
|
+
self.progress_slider.setValue(position)
|
243
|
+
|
244
|
+
def set_duration(self, duration: int) -> None:
|
245
|
+
"""Set the progress slider range based on media duration."""
|
246
|
+
self.progress_slider.setRange(0, duration)
|
247
|
+
|
248
|
+
def set_position(self, position: int) -> None:
|
249
|
+
"""Set the media position when slider is moved."""
|
250
|
+
self.setPosition(position)
|
251
|
+
|
252
|
+
def slider_released(self) -> None:
|
253
|
+
"""Handle slider release event."""
|
254
|
+
self.setPosition(self.progress_slider.value())
|
255
|
+
|
256
|
+
def play_data(self, data: bytes, name: str) -> None:
|
257
|
+
"""Play the video."""
|
258
|
+
self.stop()
|
259
|
+
self.buffer = QBuffer()
|
260
|
+
self.buffer.setData(QByteArray(data))
|
261
|
+
self.buffer.open(QBuffer.OpenModeFlag.ReadOnly)
|
262
|
+
|
263
|
+
self.setSourceDevice(self.buffer, name)
|
264
|
+
|
265
|
+
super().play()
|
@@ -0,0 +1,56 @@
|
|
1
|
+
"""Notification module.
|
2
|
+
|
3
|
+
This module contains functions to show notifications.
|
4
|
+
"""
|
5
|
+
|
6
|
+
from pyqttoast import Toast, ToastIcon, ToastPosition # type: ignore[import-untyped]
|
7
|
+
from PySide6.QtWidgets import QApplication
|
8
|
+
|
9
|
+
from winipedia_utils.text.string import value_to_truncated_string
|
10
|
+
|
11
|
+
Toast.setPosition(ToastPosition.TOP_MIDDLE)
|
12
|
+
|
13
|
+
|
14
|
+
class Notification(Toast): # type: ignore[misc]
|
15
|
+
"""Notification class."""
|
16
|
+
|
17
|
+
def __init__(
|
18
|
+
self,
|
19
|
+
title: str,
|
20
|
+
text: str,
|
21
|
+
icon: ToastIcon = ToastIcon.INFORMATION,
|
22
|
+
duration: int = 10000,
|
23
|
+
) -> None:
|
24
|
+
"""Initialize the notification.
|
25
|
+
|
26
|
+
The notification is shown in the top middle of the screen.
|
27
|
+
|
28
|
+
Args:
|
29
|
+
parent (QWidget): The parent widget.
|
30
|
+
title (str): The title of the notification.
|
31
|
+
text (str): The text of the notification.
|
32
|
+
icon (ToastIcon, optional): The icon of the notification.
|
33
|
+
duration (int, optional): The duration of the notification in milliseconds.
|
34
|
+
"""
|
35
|
+
super().__init__(QApplication.activeWindow())
|
36
|
+
self.setDuration(duration)
|
37
|
+
self.setIcon(icon)
|
38
|
+
self.set_title(title)
|
39
|
+
self.set_text(text)
|
40
|
+
|
41
|
+
def set_title(self, title: str) -> None:
|
42
|
+
"""Set the title of the notification."""
|
43
|
+
title = self.str_to_half_window_width(title)
|
44
|
+
self.setTitle(title)
|
45
|
+
|
46
|
+
def set_text(self, text: str) -> None:
|
47
|
+
"""Set the text of the notification."""
|
48
|
+
text = self.str_to_half_window_width(text)
|
49
|
+
self.setText(text)
|
50
|
+
|
51
|
+
def str_to_half_window_width(self, string: str) -> str:
|
52
|
+
"""Truncate the string to the width of the active window."""
|
53
|
+
main_window = QApplication.activeWindow()
|
54
|
+
width = main_window.width() / 2 if main_window is not None else 500
|
55
|
+
width = int(width)
|
56
|
+
return value_to_truncated_string(string, width)
|
@@ -0,0 +1 @@
|
|
1
|
+
"""__init__ module."""
|
@@ -0,0 +1 @@
|
|
1
|
+
"""__init__ module."""
|
@@ -0,0 +1,56 @@
|
|
1
|
+
"""Base window module.
|
2
|
+
|
3
|
+
This module contains the base window class for the VideoVault application.
|
4
|
+
"""
|
5
|
+
|
6
|
+
from abc import abstractmethod
|
7
|
+
from collections.abc import Generator
|
8
|
+
from types import ModuleType
|
9
|
+
from typing import final
|
10
|
+
|
11
|
+
from PySide6.QtWidgets import QMainWindow, QStackedWidget
|
12
|
+
|
13
|
+
from winipedia_utils.pyside.ui.base.base import Base as BaseUI
|
14
|
+
from winipedia_utils.pyside.ui.pages.base.base import Base as BasePage
|
15
|
+
|
16
|
+
|
17
|
+
class Base(BaseUI, QMainWindow):
|
18
|
+
"""Base window class for the VideoVault application."""
|
19
|
+
|
20
|
+
@abstractmethod
|
21
|
+
def start_page_cls(self) -> type[BasePage]:
|
22
|
+
"""Get the start page class."""
|
23
|
+
|
24
|
+
@abstractmethod
|
25
|
+
def get_pages_package(self) -> ModuleType:
|
26
|
+
"""The package to get the page classes from."""
|
27
|
+
|
28
|
+
@final
|
29
|
+
def base_setup(self) -> None:
|
30
|
+
"""Get the Qt object of the UI."""
|
31
|
+
self.setWindowTitle(self.get_display_name())
|
32
|
+
|
33
|
+
self.stack = QStackedWidget()
|
34
|
+
self.setCentralWidget(self.stack)
|
35
|
+
|
36
|
+
self.add_pages()
|
37
|
+
|
38
|
+
self.set_start_page()
|
39
|
+
|
40
|
+
@final
|
41
|
+
def add_pages(self) -> None:
|
42
|
+
"""Add the pages to the window."""
|
43
|
+
self.pages = list(self.make_pages())
|
44
|
+
for page in self.pages:
|
45
|
+
self.stack.addWidget(page)
|
46
|
+
|
47
|
+
@final
|
48
|
+
def make_pages(self) -> Generator[BasePage, None, None]:
|
49
|
+
"""Get the pages to add to the window."""
|
50
|
+
for page_cls in BasePage.get_subclasses(self.get_pages_package()):
|
51
|
+
yield page_cls()
|
52
|
+
|
53
|
+
@final
|
54
|
+
def set_start_page(self) -> None:
|
55
|
+
"""Set the start page."""
|
56
|
+
self.set_current_page(self.start_page_cls())
|
@@ -0,0 +1 @@
|
|
1
|
+
"""__init__ module."""
|
@@ -0,0 +1 @@
|
|
1
|
+
"""__init__ module."""
|
@@ -0,0 +1,7 @@
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
2
|
+
<svg width="800px" height="800px" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
3
|
+
<path d="M1 6L6 6L6 1L4.2 1L4.2 4.2L1 4.2L1 6Z" fill="#000000"/>
|
4
|
+
<path d="M15 10L10 10L10 15L11.8 15L11.8 11.8L15 11.8L15 10Z" fill="#000000"/>
|
5
|
+
<path d="M6 15L6 10L1 10L1 11.8L4.2 11.8L4.2 15L6 15Z" fill="#000000"/>
|
6
|
+
<path d="M10 1L10 6L15 6L15 4.2L11.8 4.2L11.8 1L10 1Z" fill="#000000"/>
|
7
|
+
</svg>
|
@@ -0,0 +1,4 @@
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
2
|
+
<svg width="800px" height="800px" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
3
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 15H15V10H13.2V13.2H10V15ZM6 15V13.2H2.8V10H1V15H6ZM10 2.8H12.375H13.2V6H15V1H10V2.8ZM6 1V2.8H2.8V6H1V1H6Z" fill="#000000"/>
|
4
|
+
</svg>
|
@@ -0,0 +1,8 @@
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 330 330" width="24" height="24">
|
2
|
+
<path d="M37.728,328.12c2.266,1.256,4.77,1.88,7.272,1.88
|
3
|
+
c2.763,0,5.522-0.763,7.95-2.28l240-149.999
|
4
|
+
c4.386-2.741,7.05-7.548,7.05-12.72c0-5.172-2.664-9.979-7.05-12.72
|
5
|
+
L52.95,2.28c-4.625-2.891-10.453-3.043-15.222-0.4
|
6
|
+
C32.959,4.524,30,9.547,30,15v300
|
7
|
+
C30,320.453,32.959,325.476,37.728,328.12z"/>
|
8
|
+
</svg>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
"""utils for svgs."""
|
2
|
+
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
from winipedia_utils.modules.module import to_path
|
6
|
+
from winipedia_utils.resources import svgs
|
7
|
+
|
8
|
+
|
9
|
+
def get_svg_path(svg_name: str) -> Path:
|
10
|
+
"""Get the path to a svg."""
|
11
|
+
return to_path(svgs, is_package=True) / svg_name
|
@@ -0,0 +1 @@
|
|
1
|
+
"""__init__ module for winipedia_utils.security."""
|
@@ -0,0 +1,30 @@
|
|
1
|
+
"""Keyring utilities for secure storage and retrieval of secrets.
|
2
|
+
|
3
|
+
This module provides utility functions for working with keyring,
|
4
|
+
including getting and creating secrets and fernets.
|
5
|
+
These utilities help with secure storage and retrieval of secrets.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import keyring
|
9
|
+
from cryptography.fernet import Fernet
|
10
|
+
|
11
|
+
|
12
|
+
def get_or_create_secret(service_name: str, username: str) -> str:
|
13
|
+
"""Get the app secret using keyring.
|
14
|
+
|
15
|
+
If it does not exist, create it with a Fernet.
|
16
|
+
"""
|
17
|
+
secret = keyring.get_password(service_name, username)
|
18
|
+
if secret is None:
|
19
|
+
secret = Fernet.generate_key().decode()
|
20
|
+
keyring.set_password(service_name, username, secret)
|
21
|
+
return secret
|
22
|
+
|
23
|
+
|
24
|
+
def get_or_create_fernet(service_name: str, username: str) -> Fernet:
|
25
|
+
"""Get the app fernet using keyring.
|
26
|
+
|
27
|
+
If it does not exist, create it with a Fernet.
|
28
|
+
"""
|
29
|
+
secret = get_or_create_secret(service_name, username)
|
30
|
+
return Fernet(secret.encode())
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/concurrent/multiprocessing.py
RENAMED
File without changes
|
{winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/concurrent/multithreading.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/git/gitignore/gitignore.py
RENAMED
File without changes
|
{winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/git/pre_commit/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
{winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/git/pre_commit/run_hooks.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/projects/poetry/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/testing/tests/base/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{winipedia_utils-0.1.15 → winipedia_utils-0.1.17}/winipedia_utils/testing/tests/base/utils/utils.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|