qcanvas 1.0.11__py3-none-any.whl → 2026.1.19__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.
Files changed (97) hide show
  1. qcanvas/__init__.py +60 -0
  2. qcanvas/app.py +72 -0
  3. qcanvas/backend_connectors/frontend_resource_manager.py +13 -5
  4. qcanvas/backend_connectors/qcanvas_task_master.py +2 -2
  5. qcanvas/icons/__init__.py +55 -6
  6. qcanvas/icons/_icon_type.py +42 -0
  7. qcanvas/icons/icons.qrc +48 -8
  8. qcanvas/icons/rc_icons.py +2477 -566
  9. qcanvas/settings/__init__.py +6 -0
  10. qcanvas/{util/settings → settings}/_client_settings.py +15 -6
  11. qcanvas/settings/_course_settings.py +54 -0
  12. qcanvas/{util/settings → settings}/_mapped_setting.py +8 -6
  13. qcanvas/{util/settings → settings}/_ui_settings.py +5 -5
  14. qcanvas/theme.py +101 -0
  15. qcanvas/ui/course_viewer/content_tree.py +37 -19
  16. qcanvas/ui/course_viewer/course_tree/__init__.py +1 -0
  17. qcanvas/ui/course_viewer/course_tree/_course_icon_generator.py +86 -0
  18. qcanvas/ui/course_viewer/{course_tree.py → course_tree/course_tree.py} +29 -14
  19. qcanvas/ui/course_viewer/course_viewer.py +79 -46
  20. qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tab.py +107 -29
  21. qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +19 -18
  22. qcanvas/ui/course_viewer/tabs/content_tab.py +33 -39
  23. qcanvas/ui/course_viewer/tabs/file_tab/__init__.py +1 -0
  24. qcanvas/ui/course_viewer/tabs/file_tab/file_tab.py +46 -0
  25. qcanvas/ui/course_viewer/tabs/file_tab/file_tree.py +96 -0
  26. qcanvas/ui/course_viewer/tabs/file_tab/pages_file_tree.py +55 -0
  27. qcanvas/ui/course_viewer/tabs/mail_tab/mail_tab.py +50 -27
  28. qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +18 -19
  29. qcanvas/ui/course_viewer/tabs/page_tab/page_tab.py +3 -3
  30. qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +18 -16
  31. qcanvas/ui/course_viewer/tabs/resource_rich_browser.py +61 -74
  32. qcanvas/ui/course_viewer/tree_widget_data_item.py +22 -0
  33. qcanvas/ui/memory_tree/_tree_memory.py +45 -41
  34. qcanvas/ui/memory_tree/memory_tree_widget.py +22 -18
  35. qcanvas/ui/memory_tree/memory_tree_widget_item.py +3 -3
  36. qcanvas/ui/qcanvas_window/__init__.py +1 -0
  37. qcanvas/ui/qcanvas_window/course_viewer_container.py +95 -0
  38. qcanvas/ui/{main_ui → qcanvas_window}/options/auto_download_resources_option.py +8 -6
  39. qcanvas/ui/{main_ui → qcanvas_window}/options/quick_sync_option.py +7 -6
  40. qcanvas/ui/{main_ui → qcanvas_window}/options/sync_on_start_option.py +7 -6
  41. qcanvas/ui/{main_ui → qcanvas_window}/options/theme_selection_menu.py +12 -10
  42. qcanvas/ui/{main_ui → qcanvas_window}/qcanvas_window.py +74 -45
  43. qcanvas/ui/{main_ui → qcanvas_window}/status_bar_progress_display.py +20 -12
  44. qcanvas/ui/qml_components/__init__.py +4 -0
  45. qcanvas/ui/qml_components/attachments_pane.py +70 -0
  46. qcanvas/ui/qml_components/comments_pane.py +83 -0
  47. qcanvas/ui/qml_components/qml/AttachmentsList.ui.qml +15 -0
  48. qcanvas/ui/qml_components/qml/AttachmentsListDelegate.qml +77 -0
  49. qcanvas/ui/qml_components/qml/AttachmentsListModel.qml +19 -0
  50. qcanvas/ui/qml_components/qml/AttachmentsPane.qml +11 -0
  51. qcanvas/ui/qml_components/qml/CommentsList.ui.qml +15 -0
  52. qcanvas/ui/qml_components/qml/CommentsListDelegate.ui.qml +118 -0
  53. qcanvas/ui/qml_components/qml/CommentsListModel.qml +56 -0
  54. qcanvas/ui/qml_components/qml/CommentsPane.qml +11 -0
  55. qcanvas/ui/qml_components/qml/DecoratedText.ui.qml +44 -0
  56. qcanvas/ui/qml_components/qml/Spacer.ui.qml +7 -0
  57. qcanvas/ui/qml_components/qml/ThemedRectangle.qml +53 -0
  58. qcanvas/ui/qml_components/qml/__init__.py +3 -0
  59. qcanvas/ui/qml_components/qml/rc_qml.py +709 -0
  60. qcanvas/ui/qml_components/qml/rc_qml.qrc +16 -0
  61. qcanvas/ui/qml_components/qml_bridge_types.py +95 -0
  62. qcanvas/ui/qml_components/qml_pane.py +21 -0
  63. qcanvas/ui/setup/setup_checker.py +3 -3
  64. qcanvas/ui/setup/setup_dialog.py +173 -80
  65. qcanvas/util/__init__.py +0 -2
  66. qcanvas/util/auto_downloader.py +9 -8
  67. qcanvas/util/basic_fonts.py +2 -2
  68. qcanvas/util/context_dict.py +12 -0
  69. qcanvas/util/file_icons.py +46 -0
  70. qcanvas/util/html_cleaner.py +2 -0
  71. qcanvas/util/layouts.py +9 -8
  72. qcanvas/util/paths.py +26 -22
  73. qcanvas/util/qurl_util.py +1 -1
  74. qcanvas/util/runtime.py +20 -0
  75. qcanvas/util/ui_tools.py +121 -7
  76. qcanvas/util/url_checker.py +1 -1
  77. qcanvas-2026.1.19.dist-info/METADATA +95 -0
  78. qcanvas-2026.1.19.dist-info/RECORD +92 -0
  79. {qcanvas-1.0.11.dist-info → qcanvas-2026.1.19.dist-info}/WHEEL +1 -1
  80. qcanvas-2026.1.19.dist-info/entry_points.txt +3 -0
  81. qcanvas/app_start/__init__.py +0 -54
  82. qcanvas/icons/file-download-failed.svg +0 -6
  83. qcanvas/icons/file-downloaded.svg +0 -6
  84. qcanvas/icons/file-not-downloaded.svg +0 -6
  85. qcanvas/icons/file-unknown.svg +0 -6
  86. qcanvas/icons/main_icon.svg +0 -325
  87. qcanvas/icons/sync.svg +0 -7
  88. qcanvas/run.py +0 -30
  89. qcanvas/ui/main_ui/__init__.py +0 -0
  90. qcanvas/ui/main_ui/course_viewer_container.py +0 -52
  91. qcanvas/util/settings/__init__.py +0 -9
  92. qcanvas/util/themes.py +0 -24
  93. qcanvas-1.0.11.dist-info/METADATA +0 -61
  94. qcanvas-1.0.11.dist-info/RECORD +0 -68
  95. qcanvas-1.0.11.dist-info/entry_points.txt +0 -3
  96. /qcanvas/ui/course_viewer/tabs/{util.py → constants.py} +0 -0
  97. /qcanvas/ui/{main_ui → qcanvas_window}/options/__init__.py +0 -0
qcanvas/__init__.py CHANGED
@@ -0,0 +1,60 @@
1
+ # nuitka-project: --enable-plugin=pyside6
2
+ # nuitka-project: --windows-icon-from-ico=./deploy/windows/qcanvas.ico
3
+ # nuitka-project: --windows-console-mode=attach
4
+ # nuitka-project: --linux-icon=deploy/appimage/qcanvas.svg
5
+ # nuitka-project: --standalone
6
+ # nuitka-project: --include-module=yt_dlp
7
+
8
+ # Anti-bloat
9
+
10
+ ##########################################################
11
+ # nuitka-project: --include-package=aiosqlite
12
+ # nuitka-project: --nofollow-import-to=aiosqlite.tests.*
13
+ ##########################################################
14
+ # nuitka-project: --nofollow-import-to=rich
15
+ ##########################################################
16
+ # nuitka-project: --nofollow-import-to=sqlalchemy.dialects.mssql
17
+ # nuitka-project: --nofollow-import-to=sqlalchemy.dialects.postgresql
18
+ # nuitka-project: --nofollow-import-to=sqlalchemy.dialects.oracle
19
+ # nuitka-project: --nofollow-import-to=sqlalchemy.dialects.mysql
20
+ ##########################################################
21
+ # nuitka-project: --nofollow-import-to=rich
22
+ ##########################################################
23
+ # nuitka-project: --nofollow-import-to=yt_dlp.extractor.lazy_extractors
24
+
25
+ import logging
26
+ import logging.config
27
+ from logging import INFO, WARNING
28
+
29
+ import qcanvas.app as app
30
+ from qcanvas.util import logs, paths
31
+
32
+
33
+ def main():
34
+ paths.data_storage().mkdir(parents=True, exist_ok=True)
35
+
36
+ logging.basicConfig(
37
+ filemode="w",
38
+ filename=paths.data_storage() / "qcanvas.log",
39
+ level="WARN",
40
+ )
41
+
42
+ log_config = paths.data_storage() / "logging.ini"
43
+
44
+ if log_config.exists():
45
+ logging.config.fileConfig(log_config, disable_existing_loggers=False)
46
+ else:
47
+ logs.set_levels(
48
+ {
49
+ "qcanvas": INFO,
50
+ "qcanvas.ui": WARNING,
51
+ "libqcanvas": INFO,
52
+ "qcanvas.ui.main_ui.status_bar_progress_display": INFO,
53
+ }
54
+ )
55
+
56
+ app.launch()
57
+
58
+
59
+ if __name__ == "__main__":
60
+ main()
qcanvas/app.py ADDED
@@ -0,0 +1,72 @@
1
+ import asyncio
2
+ import logging
3
+ import sys
4
+
5
+ from PySide6.QtCore import Qt
6
+ from PySide6.QtGui import QGuiApplication
7
+ from PySide6.QtWidgets import QApplication
8
+ from qasync import QEventLoop
9
+
10
+ import qcanvas.backend_connectors.qcanvas_task_master as task_master
11
+ import qcanvas.settings as settings
12
+ from libqcanvas.qcanvas import QCanvas
13
+ from qcanvas.backend_connectors import FrontendResourceManager
14
+ from qcanvas.theme import app_theme
15
+ from qcanvas.ui.qcanvas_window import QCanvasWindow
16
+ from qcanvas.ui.setup import SetupDialog, setup_checker
17
+ from qcanvas.util import paths, runtime
18
+
19
+ _logger = logging.getLogger(__name__)
20
+ app = QApplication(sys.argv)
21
+
22
+
23
+ async def setup_database() -> QCanvas[FrontendResourceManager]:
24
+ _qcanvas = QCanvas[FrontendResourceManager](
25
+ canvas_config=settings.client.canvas_config,
26
+ panopto_config=settings.client.panopto_config,
27
+ storage_path=paths.data_storage(),
28
+ resource_manager_class=FrontendResourceManager,
29
+ )
30
+
31
+ await _qcanvas.database.upgrade()
32
+ await _qcanvas.init()
33
+
34
+ return _qcanvas
35
+
36
+
37
+ def run_setup():
38
+ app_close_event = asyncio.Event()
39
+ app.aboutToQuit.connect(app_close_event.set, Qt.ConnectionType.SingleShotConnection)
40
+
41
+ async def coro():
42
+ setup_window = SetupDialog()
43
+ setup_window.rejected.connect(lambda: exit())
44
+ setup_window.show()
45
+ await app_close_event.wait()
46
+
47
+ asyncio.run(coro(), loop_factory=QEventLoop)
48
+
49
+
50
+ def launch():
51
+ if runtime.is_running_as_flatpak:
52
+ QGuiApplication.setDesktopFileName("io.github.qcanvas.QCanvasApp")
53
+
54
+ app.setApplicationName("QCanvas")
55
+
56
+ task_master.register()
57
+ app_theme.theme = settings.ui.theme
58
+
59
+ if setup_checker.needs_setup():
60
+ run_setup()
61
+
62
+ app_close_event = asyncio.Event()
63
+ app.aboutToQuit.connect(app_close_event.set, Qt.ConnectionType.SingleShotConnection)
64
+
65
+ async def async_main():
66
+ _qcanvas = await setup_database()
67
+
68
+ _main_window = QCanvasWindow(_qcanvas)
69
+ _main_window.show()
70
+ await app_close_event.wait()
71
+
72
+ asyncio.run(async_main(), loop_factory=QEventLoop)
@@ -2,11 +2,14 @@ import logging
2
2
  from abc import ABCMeta
3
3
  from pathlib import Path
4
4
 
5
- import qcanvas_backend.database.types as db
6
- from qcanvas_backend.database import QCanvasDatabase
7
- from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
8
- from qcanvas_backend.net.resources.extracting.extractors import Extractors
9
- from qtpy.QtCore import QObject, Signal
5
+ from PySide6.QtGui import QDesktopServices
6
+ from libqcanvas import db
7
+ from libqcanvas.database import QCanvasDatabase
8
+ from libqcanvas.net.resources.download.resource_manager import ResourceManager
9
+ from libqcanvas.net.resources.extracting.extractors import Extractors
10
+ from PySide6.QtCore import QObject, Signal
11
+
12
+ from qcanvas.util.qurl_util import file_url
10
13
 
11
14
  _logger = logging.getLogger(__name__)
12
15
 
@@ -29,6 +32,11 @@ class FrontendResourceManager(QObject, ResourceManager, metaclass=_Meta):
29
32
  database=database, download_dest=download_dest, extractors=extractors
30
33
  )
31
34
 
35
+ async def download_and_open(self, resource: db.Resource) -> None:
36
+ await self.download(resource)
37
+ resource_path = file_url(self.resource_download_location(resource))
38
+ QDesktopServices.openUrl(resource_path)
39
+
32
40
  def on_download_progress(
33
41
  self, resource: db.Resource, current: int, total: int
34
42
  ) -> None:
@@ -1,8 +1,8 @@
1
1
  import logging
2
2
  from abc import ABCMeta
3
3
 
4
- from qcanvas_backend.task_master import TaskID, TaskMaster, set_global_task_master
5
- from qtpy.QtCore import QObject, Signal
4
+ from libqcanvas.task_master import TaskID, TaskMaster, set_global_task_master
5
+ from PySide6.QtCore import QObject, Signal
6
6
 
7
7
  _logger = logging.getLogger(__name__)
8
8
 
qcanvas/icons/__init__.py CHANGED
@@ -1,7 +1,56 @@
1
- from qcanvas.icons import rc_icons
1
+ from .rc_icons import (
2
+ qt_resource_data as _,
3
+ ) # Without this, icon data will not be loaded
4
+ from ._icon_type import UniversalIcon, ThemeIcon
5
+ from PySide6.QtGui import QIcon
2
6
 
3
- main_icon = ":/main_icon.svg"
4
- file_downloaded = ":/file-downloaded.svg"
5
- file_not_downloaded = ":/file-not-downloaded.svg"
6
- file_download_failed = ":/file-download-failed.svg"
7
- file_unknown = ":/file-unknown.svg"
7
+
8
+ # noinspection PyPep8Naming
9
+ class actions:
10
+ exit = QIcon.fromTheme("actions/exit")
11
+ mark_all_read = QIcon.fromTheme("actions/mark_all_read")
12
+ open_downloads = QIcon.fromTheme("actions/open_downloads")
13
+ quick_login = QIcon.fromTheme("actions/quick_login")
14
+ sync = QIcon.fromTheme("actions/sync")
15
+
16
+
17
+ # noinspection PyPep8Naming
18
+ class branding:
19
+ logo_transparent = QIcon.fromTheme("branding/logo_transparent")
20
+ main_icon = QIcon.fromTheme("branding/main_icon")
21
+
22
+
23
+ # noinspection PyPep8Naming
24
+ class options:
25
+ auto_download = QIcon.fromTheme("options/auto_download")
26
+ ignore_old = QIcon.fromTheme("options/ignore_old")
27
+ include_videos = QIcon.fromTheme("options/include_videos")
28
+ sync_on_start = QIcon.fromTheme("options/sync_on_start")
29
+ theme = QIcon.fromTheme("options/theme")
30
+
31
+
32
+ # noinspection PyPep8Naming
33
+ class tabs:
34
+ assignments = QIcon.fromTheme("tabs/assignments")
35
+ assignments_new_content = QIcon.fromTheme("tabs/assignments_new_content")
36
+ mail = QIcon.fromTheme("tabs/mail")
37
+ mail_new_content = QIcon.fromTheme("tabs/mail_new_content")
38
+ pages = QIcon.fromTheme("tabs/pages")
39
+ pages_new_content = QIcon.fromTheme("tabs/pages_new_content")
40
+
41
+
42
+ # noinspection PyPep8Naming
43
+ class tree_items:
44
+ assignment = QIcon.fromTheme("tree_items/assignment")
45
+ mail = QIcon.fromTheme("tree_items/mail")
46
+ module = QIcon.fromTheme("tree_items/module")
47
+ page = QIcon.fromTheme("tree_items/page")
48
+ semester = QIcon.fromTheme("tree_items/semester")
49
+
50
+
51
+ # noinspection PyPep8Naming
52
+ class downloads:
53
+ download_failed = QIcon.fromTheme("downloads/download_failed")
54
+ downloaded = QIcon.fromTheme("downloads/downloaded")
55
+ not_downloaded = QIcon.fromTheme("downloads/not_downloaded")
56
+ unknown = QIcon.fromTheme("downloads/unknown")
@@ -0,0 +1,42 @@
1
+ # Unused
2
+ import logging
3
+
4
+ from PySide6.QtGui import QIcon
5
+
6
+ _logger = logging.getLogger(__name__)
7
+
8
+
9
+ class ThemeIcon:
10
+ def __init__(self, theme_path: str):
11
+ self._theme_path = theme_path
12
+ self._icon = None
13
+
14
+ @property
15
+ def icon(self) -> QIcon:
16
+ if self._icon is None:
17
+ self._icon = QIcon.fromTheme(self._theme_path)
18
+
19
+ return self._icon
20
+
21
+ @property
22
+ def theme_path(self) -> str:
23
+ return self._theme_path
24
+
25
+ def __hash__(self) -> int:
26
+ return hash(self._theme_path)
27
+
28
+
29
+ class UniversalIcon(ThemeIcon):
30
+ def __init__(self, theme_path: str, full_path: str):
31
+ super().__init__(theme_path)
32
+ self._full_path = full_path
33
+
34
+ @property
35
+ def full_path(self) -> str:
36
+ return self._full_path
37
+
38
+ def __hash__(self) -> int:
39
+ return hash(self._theme_path) ^ hash(self.full_path)
40
+
41
+
42
+ AnyIcon = UniversalIcon | ThemeIcon
qcanvas/icons/icons.qrc CHANGED
@@ -1,10 +1,50 @@
1
1
  <!DOCTYPE RCC>
2
2
  <RCC version="1.0">
3
- <qresource>
4
- <file>main_icon.svg</file>
5
- <file>file-downloaded.svg</file>
6
- <file>file-not-downloaded.svg</file>
7
- <file>file-download-failed.svg</file>
8
- <file>file-unknown.svg</file>
9
- </qresource>
10
- </RCC>
3
+ <qresource prefix="icons/">
4
+ <file>dark/actions/exit.svg</file>
5
+ <file>dark/actions/mark_all_read.svg</file>
6
+ <file>dark/actions/open_downloads.svg</file>
7
+ <file>dark/actions/quick_login.svg</file>
8
+ <file>dark/actions/sync.svg</file>
9
+ <file>dark/branding/logo_transparent.svg</file>
10
+ <file>dark/options/auto_download.svg</file>
11
+ <file>dark/options/ignore_old.svg</file>
12
+ <file>dark/options/include_videos.svg</file>
13
+ <file>dark/options/sync_on_start.svg</file>
14
+ <file>dark/options/theme.svg</file>
15
+ <file>dark/tabs/assignments.svg</file>
16
+ <file>dark/tabs/mail.svg</file>
17
+ <file>dark/tabs/pages.svg</file>
18
+ <file>dark/tree_items/assignment.svg</file>
19
+ <file>dark/tree_items/mail.svg</file>
20
+ <file>dark/tree_items/module.svg</file>
21
+ <file>dark/tree_items/page.svg</file>
22
+ <file>light/actions/exit.svg</file>
23
+ <file>light/actions/mark_all_read.svg</file>
24
+ <file>light/actions/open_downloads.svg</file>
25
+ <file>light/actions/quick_login.svg</file>
26
+ <file>light/actions/sync.svg</file>
27
+ <file>light/branding/logo_transparent.svg</file>
28
+ <file>light/options/auto_download.svg</file>
29
+ <file>light/options/ignore_old.svg</file>
30
+ <file>light/options/include_videos.svg</file>
31
+ <file>light/options/sync_on_start.svg</file>
32
+ <file>light/options/theme.svg</file>
33
+ <file>light/tabs/assignments.svg</file>
34
+ <file>light/tabs/mail.svg</file>
35
+ <file>light/tabs/pages.svg</file>
36
+ <file>light/tree_items/assignment.svg</file>
37
+ <file>light/tree_items/mail.svg</file>
38
+ <file>light/tree_items/module.svg</file>
39
+ <file>light/tree_items/page.svg</file>
40
+ <file>universal/branding/main_icon.svg</file>
41
+ <file>universal/downloads/download_failed.svg</file>
42
+ <file>universal/downloads/downloaded.svg</file>
43
+ <file>universal/downloads/not_downloaded.svg</file>
44
+ <file>universal/downloads/unknown.svg</file>
45
+ <file>universal/tabs/assignments_new_content.svg</file>
46
+ <file>universal/tabs/mail_new_content.svg</file>
47
+ <file>universal/tabs/pages_new_content.svg</file>
48
+ <file>universal/tree_items/semester.svg</file>
49
+ </qresource>
50
+ </RCC>