qcanvas 1.2.0__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 (132) 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 +5 -5
  6. qcanvas/icons/_icon_type.py +1 -1
  7. qcanvas/icons/icons.qrc +39 -35
  8. qcanvas/icons/rc_icons.py +1298 -1197
  9. qcanvas/settings/__init__.py +6 -0
  10. qcanvas/{util/settings → settings}/_client_settings.py +4 -4
  11. qcanvas/settings/_course_settings.py +54 -0
  12. qcanvas/{util/settings → settings}/_mapped_setting.py +2 -5
  13. qcanvas/{util/settings → settings}/_ui_settings.py +5 -5
  14. qcanvas/theme.py +101 -0
  15. qcanvas/ui/course_viewer/content_tree.py +9 -12
  16. qcanvas/ui/course_viewer/course_tree/_course_icon_generator.py +3 -3
  17. qcanvas/ui/course_viewer/course_tree/course_tree.py +9 -8
  18. qcanvas/ui/course_viewer/course_viewer.py +42 -56
  19. qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tab.py +107 -29
  20. qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +4 -4
  21. qcanvas/ui/course_viewer/tabs/constants.py +1 -0
  22. qcanvas/ui/course_viewer/tabs/content_tab.py +33 -39
  23. qcanvas/ui/course_viewer/tabs/file_tab/file_tab.py +4 -4
  24. qcanvas/ui/course_viewer/tabs/file_tab/file_tree.py +7 -10
  25. qcanvas/ui/course_viewer/tabs/file_tab/pages_file_tree.py +6 -7
  26. qcanvas/ui/course_viewer/tabs/mail_tab/mail_tab.py +50 -27
  27. qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +7 -8
  28. qcanvas/ui/course_viewer/tabs/page_tab/page_tab.py +3 -3
  29. qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +5 -5
  30. qcanvas/ui/course_viewer/tabs/resource_rich_browser.py +18 -32
  31. qcanvas/ui/course_viewer/tree_widget_data_item.py +1 -1
  32. qcanvas/ui/memory_tree/_tree_memory.py +45 -42
  33. qcanvas/ui/memory_tree/memory_tree_widget.py +22 -18
  34. qcanvas/ui/memory_tree/memory_tree_widget_item.py +3 -3
  35. qcanvas/ui/qcanvas_window/__init__.py +1 -0
  36. qcanvas/ui/{main_ui → qcanvas_window}/course_viewer_container.py +10 -10
  37. qcanvas/ui/{main_ui → qcanvas_window}/options/auto_download_resources_option.py +5 -5
  38. qcanvas/ui/{main_ui → qcanvas_window}/options/quick_sync_option.py +7 -6
  39. qcanvas/ui/{main_ui → qcanvas_window}/options/sync_on_start_option.py +7 -6
  40. qcanvas/ui/{main_ui → qcanvas_window}/options/theme_selection_menu.py +10 -10
  41. qcanvas/ui/{main_ui → qcanvas_window}/qcanvas_window.py +57 -41
  42. qcanvas/ui/{main_ui → qcanvas_window}/status_bar_progress_display.py +5 -6
  43. qcanvas/ui/qml_components/__init__.py +4 -0
  44. qcanvas/ui/qml_components/attachments_pane.py +70 -0
  45. qcanvas/ui/qml_components/comments_pane.py +83 -0
  46. qcanvas/ui/qml_components/qml/AttachmentsList.ui.qml +15 -0
  47. qcanvas/ui/qml_components/qml/AttachmentsListDelegate.qml +77 -0
  48. qcanvas/ui/qml_components/qml/AttachmentsListModel.qml +19 -0
  49. qcanvas/ui/qml_components/qml/AttachmentsPane.qml +11 -0
  50. qcanvas/ui/qml_components/qml/CommentsList.ui.qml +15 -0
  51. qcanvas/ui/qml_components/qml/CommentsListDelegate.ui.qml +118 -0
  52. qcanvas/ui/qml_components/qml/CommentsListModel.qml +56 -0
  53. qcanvas/ui/qml_components/qml/CommentsPane.qml +11 -0
  54. qcanvas/ui/qml_components/qml/DecoratedText.ui.qml +44 -0
  55. qcanvas/ui/qml_components/qml/Spacer.ui.qml +7 -0
  56. qcanvas/ui/qml_components/qml/ThemedRectangle.qml +53 -0
  57. qcanvas/ui/qml_components/qml/__init__.py +3 -0
  58. qcanvas/ui/qml_components/qml/rc_qml.py +709 -0
  59. qcanvas/ui/qml_components/qml/rc_qml.qrc +16 -0
  60. qcanvas/ui/qml_components/qml_bridge_types.py +95 -0
  61. qcanvas/ui/qml_components/qml_pane.py +21 -0
  62. qcanvas/ui/setup/setup_checker.py +1 -1
  63. qcanvas/ui/setup/setup_dialog.py +28 -14
  64. qcanvas/util/auto_downloader.py +9 -7
  65. qcanvas/util/basic_fonts.py +2 -2
  66. qcanvas/util/context_dict.py +12 -0
  67. qcanvas/util/file_icons.py +11 -19
  68. qcanvas/util/layouts.py +5 -7
  69. qcanvas/util/paths.py +17 -6
  70. qcanvas/util/qurl_util.py +1 -1
  71. qcanvas/util/ui_tools.py +118 -8
  72. qcanvas/util/url_checker.py +1 -1
  73. qcanvas-2026.1.19.dist-info/METADATA +95 -0
  74. qcanvas-2026.1.19.dist-info/RECORD +92 -0
  75. {qcanvas-1.2.0.dist-info → qcanvas-2026.1.19.dist-info}/WHEEL +1 -1
  76. qcanvas-2026.1.19.dist-info/entry_points.txt +3 -0
  77. qcanvas/app_start/__init__.py +0 -59
  78. qcanvas/icons/_update_icons.py +0 -89
  79. qcanvas/icons/dark/actions/exit.svg +0 -3
  80. qcanvas/icons/dark/actions/mark_all_read.svg +0 -3
  81. qcanvas/icons/dark/actions/open_downloads.svg +0 -3
  82. qcanvas/icons/dark/actions/quick_login.svg +0 -3
  83. qcanvas/icons/dark/actions/sync.svg +0 -3
  84. qcanvas/icons/dark/branding/logo_transparent.svg +0 -303
  85. qcanvas/icons/dark/options/auto_download.svg +0 -3
  86. qcanvas/icons/dark/options/theme.svg +0 -3
  87. qcanvas/icons/dark/tabs/assignments.svg +0 -3
  88. qcanvas/icons/dark/tabs/mail.svg +0 -3
  89. qcanvas/icons/dark/tabs/pages.svg +0 -3
  90. qcanvas/icons/dark/tree_items/assignment.svg +0 -3
  91. qcanvas/icons/dark/tree_items/mail.svg +0 -3
  92. qcanvas/icons/dark/tree_items/module.svg +0 -3
  93. qcanvas/icons/dark/tree_items/page.svg +0 -3
  94. qcanvas/icons/light/actions/exit.svg +0 -3
  95. qcanvas/icons/light/actions/mark_all_read.svg +0 -3
  96. qcanvas/icons/light/actions/open_downloads.svg +0 -3
  97. qcanvas/icons/light/actions/quick_login.svg +0 -3
  98. qcanvas/icons/light/actions/sync.svg +0 -3
  99. qcanvas/icons/light/branding/logo_transparent.svg +0 -304
  100. qcanvas/icons/light/options/auto_download.svg +0 -3
  101. qcanvas/icons/light/options/ignore_old.svg +0 -3
  102. qcanvas/icons/light/options/include_videos.svg +0 -3
  103. qcanvas/icons/light/options/theme.svg +0 -3
  104. qcanvas/icons/light/tabs/assignments.svg +0 -3
  105. qcanvas/icons/light/tabs/mail.svg +0 -3
  106. qcanvas/icons/light/tabs/pages.svg +0 -3
  107. qcanvas/icons/light/tree_items/assignment.svg +0 -3
  108. qcanvas/icons/light/tree_items/mail.svg +0 -3
  109. qcanvas/icons/light/tree_items/module.svg +0 -3
  110. qcanvas/icons/light/tree_items/page.svg +0 -3
  111. qcanvas/icons/universal/branding/main_icon.svg +0 -325
  112. qcanvas/icons/universal/downloads/download_failed.svg +0 -23
  113. qcanvas/icons/universal/downloads/downloaded.svg +0 -23
  114. qcanvas/icons/universal/downloads/not_downloaded.svg +0 -23
  115. qcanvas/icons/universal/downloads/unknown.svg +0 -6
  116. qcanvas/icons/universal/tabs/assignments_new_content.svg +0 -3
  117. qcanvas/icons/universal/tabs/mail_new_content.svg +0 -3
  118. qcanvas/icons/universal/tabs/pages_new_content.svg +0 -3
  119. qcanvas/icons/universal/tree_items/semester.svg +0 -108
  120. qcanvas/run.py +0 -54
  121. qcanvas/ui/course_viewer/tabs/util.py +0 -11
  122. qcanvas/ui/main_ui/__init__.py +0 -0
  123. qcanvas/util/settings/__init__.py +0 -9
  124. qcanvas/util/themes/__init__.py +0 -2
  125. qcanvas/util/themes/_colour_scheme_helper.py +0 -38
  126. qcanvas/util/themes/_selected_theme.py +0 -10
  127. qcanvas/util/themes/_theme_changed_event.py +0 -17
  128. qcanvas/util/themes/_theme_changer.py +0 -86
  129. qcanvas-1.2.0.dist-info/METADATA +0 -71
  130. qcanvas-1.2.0.dist-info/RECORD +0 -118
  131. qcanvas-1.2.0.dist-info/entry_points.txt +0 -3
  132. /qcanvas/ui/{main_ui → qcanvas_window}/options/__init__.py +0 -0
@@ -0,0 +1,95 @@
1
+ from PySide6.QtCore import Property, QObject, Signal
2
+ from PySide6.QtQml import ListProperty, QmlElement
3
+ from libqcanvas.database.tables import ResourceDownloadState
4
+
5
+
6
+ QML_IMPORT_NAME = "QCanvas"
7
+ QML_IMPORT_MAJOR_VERSION = 1
8
+
9
+
10
+ @QmlElement
11
+ class Attachment(QObject):
12
+ file_name_changed = Signal()
13
+ resource_id_changed = Signal()
14
+ download_state_changed = Signal()
15
+
16
+ # Emitted by AttachmentsListDelegate when the user clicks on an attachment
17
+ opened = Signal(str)
18
+
19
+ def __init__(
20
+ self,
21
+ file_name: str,
22
+ resource_id: str,
23
+ download_state: ResourceDownloadState,
24
+ parent: QObject | None = None,
25
+ ):
26
+ super().__init__(parent)
27
+ self._file_name = file_name
28
+ self._resource_id = resource_id
29
+ self._download_state = download_state.name
30
+
31
+ @Property(str, notify=file_name_changed)
32
+ def file_name(self) -> str:
33
+ return self._file_name
34
+
35
+ @Property(str, notify=resource_id_changed)
36
+ def resource_id(self) -> str:
37
+ return self._resource_id
38
+
39
+ @Property(str, notify=download_state_changed)
40
+ def download_state(self) -> str:
41
+ return self._download_state
42
+
43
+ @download_state.setter
44
+ def download_state(self, value: ResourceDownloadState):
45
+ if value.name != self._download_state:
46
+ self._download_state = value.name
47
+ self.download_state_changed.emit()
48
+
49
+
50
+ @QmlElement
51
+ class Comment(QObject):
52
+ body_changed = Signal()
53
+ author_changed = Signal()
54
+ date_changed = Signal()
55
+ attachments_changed = Signal()
56
+
57
+ def __init__(
58
+ self,
59
+ body: str,
60
+ author: str,
61
+ date: str,
62
+ attachments: list[Attachment],
63
+ parent: QObject | None = None,
64
+ ):
65
+ super().__init__(parent)
66
+
67
+ self._body = body
68
+ self._author = author
69
+ self._date = date
70
+ self._attachments = attachments
71
+
72
+ @Property(str, notify=body_changed)
73
+ def body(self) -> str:
74
+ return self._body
75
+
76
+ @Property(str, notify=date_changed)
77
+ def date(self) -> str:
78
+ return self._date
79
+
80
+ @Property(str, notify=author_changed)
81
+ def author(self) -> str:
82
+ return self._author
83
+
84
+ def attachment(self, n) -> Attachment:
85
+ return self._attachments[n]
86
+
87
+ def attachment_count(self) -> int:
88
+ return len(self._attachments)
89
+
90
+ # You must set `count`, `at` and `notify` EXPLICTLY (even if you name them according to the examples, which examples are all inconsistent and wrong).
91
+ # Qt?? Are you ok???
92
+ # Oh and you have to use `.length` instead of `.count` on the QML side because javascript.
93
+ attachments = ListProperty(
94
+ Attachment, count=attachment_count, at=attachment, notify=attachments_changed
95
+ )
@@ -0,0 +1,21 @@
1
+ from PySide6.QtCore import QUrl
2
+ from PySide6.QtQuick import QQuickView
3
+ from PySide6.QtWidgets import QGroupBox, QWidget
4
+
5
+ from qcanvas.theme import app_theme
6
+ from qcanvas.util.context_dict import ContextDict
7
+ import qcanvas.util.ui_tools as ui
8
+
9
+
10
+ class QmlPane(QGroupBox):
11
+ def __init__(self, qml_path: QUrl, parent: QWidget | None = None):
12
+ super().__init__(parent)
13
+ self.qview = QQuickView(parent)
14
+ self._qml_path = qml_path
15
+ self.ctx = ContextDict(self.qview.rootContext())
16
+ self.ctx["appTheme"] = app_theme
17
+
18
+ self.setLayout(ui.hbox(QWidget.createWindowContainer(self.qview, self)))
19
+
20
+ def load_view(self):
21
+ self.qview.setSource(self._qml_path)
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
 
3
- import qcanvas.util.settings as settings
3
+ import qcanvas.settings as settings
4
4
  from qcanvas.util.url_checker import is_url
5
5
 
6
6
  _logger = logging.getLogger(__name__)
@@ -2,15 +2,26 @@ import logging
2
2
  from threading import Semaphore
3
3
  from typing import Optional
4
4
 
5
+ from libqcanvas_clients.canvas import CanvasClient, CanvasClientConfig
6
+ from libqcanvas_clients.panopto import PanoptoClient, PanoptoClientConfig
7
+ from libqcanvas_clients.util.request_exceptions import ConfigInvalidError
8
+ from PySide6.QtCore import Qt, QUrl, Slot
9
+ from PySide6.QtGui import QDesktopServices, QIcon
10
+ from PySide6.QtWidgets import (
11
+ QCheckBox,
12
+ QDialog,
13
+ QDialogButtonBox,
14
+ QErrorMessage,
15
+ QLabel,
16
+ QLineEdit,
17
+ QMessageBox,
18
+ QProgressBar,
19
+ QVBoxLayout,
20
+ QWidget,
21
+ )
5
22
  from qasync import asyncSlot
6
- from qcanvas_api_clients.canvas import CanvasClient, CanvasClientConfig
7
- from qcanvas_api_clients.panopto import PanoptoClient, PanoptoClientConfig
8
- from qcanvas_api_clients.util.request_exceptions import ConfigInvalidError
9
- from qtpy.QtCore import Qt, QUrl, Signal, Slot
10
- from qtpy.QtGui import QDesktopServices, QIcon
11
- from qtpy.QtWidgets import *
12
-
13
- import qcanvas.util.settings as settings
23
+
24
+ import qcanvas.settings as settings
14
25
  from qcanvas import icons
15
26
  from qcanvas.util.layouts import GridItem, grid_layout_widget, layout
16
27
  from qcanvas.util.url_checker import is_url
@@ -85,8 +96,6 @@ class _InputRow:
85
96
 
86
97
 
87
98
  class SetupDialog(QDialog):
88
- closed = Signal()
89
-
90
99
  def __init__(self):
91
100
  super().__init__()
92
101
 
@@ -113,9 +122,11 @@ class SetupDialog(QDialog):
113
122
  is_password=True,
114
123
  )
115
124
  self._disable_panopto_checkbox = QCheckBox("Continue without Panopto")
125
+ self._disable_panopto_checkbox.setChecked(settings.client.panopto_disabled)
116
126
  self._disable_panopto_checkbox.checkStateChanged.connect(
117
127
  self._disable_panopto_check_changed
118
128
  )
129
+ self._panopto_url_box.enabled = not settings.client.panopto_disabled
119
130
  self._button_box = self._setup_button_box()
120
131
  self._waiting_indicator = self._setup_progress_bar()
121
132
 
@@ -254,7 +265,9 @@ class SetupDialog(QDialog):
254
265
  QMessageBox.StandardButton.Ok,
255
266
  self,
256
267
  )
257
- msg.accepted.connect(self._open_panopto_login)
268
+ msg.accepted.connect(
269
+ self._open_panopto_login, Qt.ConnectionType.SingleShotConnection
270
+ )
258
271
  msg.show()
259
272
 
260
273
  @Slot()
@@ -274,8 +287,7 @@ class SetupDialog(QDialog):
274
287
 
275
288
  settings.client.canvas_api_key = self._canvas_api_key_box.text
276
289
 
277
- self.closed.emit()
278
- self.close()
290
+ self.accept()
279
291
 
280
292
  @Slot()
281
293
  def _help_requested(self) -> None:
@@ -288,7 +300,9 @@ class SetupDialog(QDialog):
288
300
  "Don't share this key. You can revoke it at any time.",
289
301
  parent=self,
290
302
  )
291
- msg.accepted.connect(self._open_tutorial)
303
+ msg.accepted.connect(
304
+ self._open_tutorial, Qt.ConnectionType.SingleShotConnection
305
+ )
292
306
  msg.show()
293
307
 
294
308
  @Slot()
@@ -1,12 +1,13 @@
1
1
  import asyncio
2
2
  import logging
3
3
 
4
- import qcanvas_backend.database.types as db
5
- from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
6
- from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
7
- from qtpy.QtWidgets import QMessageBox, QWidget
4
+ from PySide6.QtCore import Qt
5
+ from libqcanvas import db
6
+ from libqcanvas.net.resources.download.resource_manager import ResourceManager
7
+ from libqcanvas.net.sync.sync_receipt import SyncReceipt
8
+ from PySide6.QtWidgets import QMessageBox, QWidget
8
9
 
9
- from qcanvas.util import settings
10
+ import qcanvas.settings as settings
10
11
 
11
12
  _logger = logging.getLogger(__name__)
12
13
 
@@ -20,7 +21,7 @@ async def download_new_resources(
20
21
  ) -> None:
21
22
  resources_to_download = []
22
23
 
23
- for file_id in receipt.updated_resources:
24
+ for file_id in receipt.updates[db.Resource]:
24
25
  resource = all_resources[file_id]
25
26
 
26
27
  if _should_auto_download_resource(resource, resource_manager=downloader):
@@ -36,7 +37,8 @@ async def download_new_resources(
36
37
  msg.accepted.connect(
37
38
  lambda: asyncio.get_running_loop().create_task(
38
39
  downloader.batch_download(resources_to_download),
39
- )
40
+ ),
41
+ Qt.ConnectionType.SingleShotConnection,
40
42
  )
41
43
  else:
42
44
  await downloader.batch_download(resources_to_download)
@@ -1,5 +1,5 @@
1
- from qtpy.QtGui import QFont
2
- from qtpy.QtWidgets import QLabel
1
+ from PySide6.QtGui import QFont
2
+ from PySide6.QtWidgets import QLabel
3
3
 
4
4
  normal_font = QFont()
5
5
  bold_font = QFont()
@@ -0,0 +1,12 @@
1
+ from PySide6.QtQml import QQmlContext
2
+
3
+
4
+ class ContextDict:
5
+ def __init__(self, context: QQmlContext):
6
+ self._context = context
7
+
8
+ def __getitem__(self, item):
9
+ return self._context.contextProperty(item)
10
+
11
+ def __setitem__(self, key, value):
12
+ self._context.setContextProperty(key, value)
@@ -1,9 +1,10 @@
1
1
  import logging
2
2
  import os.path
3
3
 
4
- from qtpy.QtCore import QFileInfo, QMimeDatabase
5
- from qtpy.QtGui import QIcon
6
- from qtpy.QtWidgets import QApplication, QFileIconProvider, QStyle
4
+ import cachetools
5
+ from PySide6.QtCore import QFileInfo, QMimeDatabase
6
+ from PySide6.QtGui import QIcon
7
+ from PySide6.QtWidgets import QApplication, QFileIconProvider, QStyle
7
8
 
8
9
  import qcanvas.util.runtime as runtime
9
10
 
@@ -18,13 +19,9 @@ if runtime.is_running_on_windows:
18
19
 
19
20
  else:
20
21
  _mime_database = QMimeDatabase()
21
- # This must be initialised lazily because the QApplication might not be initialised at this time
22
- _default_icon: QIcon | None = None
23
22
  _icon_for_suffix: dict[str, QIcon] = {}
24
23
 
25
24
  def icon_for_filename(file_name: str) -> QIcon:
26
- global _default_icon
27
-
28
25
  file_suffix = os.path.splitext(file_name)[1]
29
26
 
30
27
  # Check if we already know what icon this file type has
@@ -39,16 +36,11 @@ else:
39
36
  _icon_for_suffix[file_suffix] = icon
40
37
  return icon
41
38
 
42
- _lazy_init_default_icon()
43
-
44
- # No icon for this type of file was found, return default icon
45
- _icon_for_suffix[file_suffix] = _default_icon
46
- return _default_icon
47
-
48
- def _lazy_init_default_icon() -> None:
49
- global _default_icon
39
+ # No icon for this type of file was found, use default icon
40
+ icon = _default_icon()
41
+ _icon_for_suffix[file_suffix] = icon
42
+ return icon
50
43
 
51
- if _default_icon is None:
52
- _default_icon = QApplication.style().standardIcon(
53
- QStyle.StandardPixmap.SP_FileIcon
54
- )
44
+ @cachetools.cached(cachetools.LRUCache(maxsize=1))
45
+ def _default_icon() -> QIcon:
46
+ return QApplication.style().standardIcon(QStyle.StandardPixmap.SP_FileIcon)
qcanvas/util/layouts.py CHANGED
@@ -1,13 +1,11 @@
1
1
  import logging
2
- from typing import *
2
+ from typing import Iterable, NamedTuple
3
3
 
4
- from qtpy.QtCore import Qt
5
- from qtpy.QtWidgets import *
4
+ from PySide6.QtCore import Qt
5
+ from PySide6.QtWidgets import QGridLayout, QLayout, QWidget
6
6
 
7
7
  _logger = logging.getLogger(__name__)
8
8
 
9
- T = TypeVar("T")
10
-
11
9
 
12
10
  class GridItem(NamedTuple):
13
11
  widget: QWidget
@@ -16,13 +14,13 @@ class GridItem(NamedTuple):
16
14
  alignment: Qt.AlignmentFlag = Qt.AlignmentFlag.AlignLeft
17
15
 
18
16
 
19
- def layout_widget(layout_type: Type[T], *items: QWidget, **kwargs) -> QWidget:
17
+ def layout_widget[T](layout_type: type[T], *items: QWidget, **kwargs) -> QWidget:
20
18
  widget = QWidget()
21
19
  widget.setLayout(layout(layout_type, *items, **kwargs))
22
20
  return widget
23
21
 
24
22
 
25
- def layout(layout_type: Type[T], *items: QWidget | QLayout, **kwargs) -> T:
23
+ def layout[T](layout_type: type[T], *items: QWidget | QLayout, **kwargs) -> T:
26
24
  result_layout: QLayout = layout_type(**kwargs)
27
25
 
28
26
  for item in items:
qcanvas/util/paths.py CHANGED
@@ -4,21 +4,30 @@ from pathlib import Path
4
4
 
5
5
  import cachetools
6
6
  import platformdirs
7
- from qtpy.QtCore import QSettings
7
+ from PySide6.QtCore import QSettings
8
8
 
9
- from qcanvas.util.runtime import *
9
+ from qcanvas.util.runtime import (
10
+ is_running_as_compiled,
11
+ is_running_as_flatpak,
12
+ is_running_portable,
13
+ )
10
14
 
11
15
  _logger = logging.getLogger(__name__)
12
16
 
13
17
 
14
- def ui_storage() -> Path:
15
- return root() / ".UI"
16
-
17
-
18
18
  def data_storage() -> Path:
19
19
  return root()
20
20
 
21
21
 
22
+ def config_storage() -> Path:
23
+ if is_running_portable:
24
+ return root()
25
+ else:
26
+ path = platformdirs.user_config_path("QCanvasTeam", "QCanvas")
27
+ path.mkdir(parents=True, exist_ok=True)
28
+ return path
29
+
30
+
22
31
  def client_settings() -> QSettings:
23
32
  if is_running_portable:
24
33
  return QSettings("QCanvas.ini", QSettings.Format.IniFormat)
@@ -35,6 +44,8 @@ def root() -> Path:
35
44
  root_path = Path(os.environ["XDG_DATA_HOME"])
36
45
  elif not is_running_portable and is_running_as_compiled:
37
46
  root_path = platformdirs.user_data_path("QCanvasReborn", "QCanvasTeam")
47
+ elif not is_running_portable:
48
+ _logger.warning("Don't know how we're being run? Are you running from source?")
38
49
 
39
50
  print("Root path", root_path.absolute())
40
51
  _logger.debug("Root path %s", root_path.absolute())
qcanvas/util/qurl_util.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import logging
2
2
  from pathlib import Path
3
3
 
4
- from qtpy.QtCore import QUrl
4
+ from PySide6.QtCore import QUrl
5
5
 
6
6
  _logger = logging.getLogger(__name__)
7
7
 
qcanvas/util/ui_tools.py CHANGED
@@ -1,14 +1,124 @@
1
- import logging
2
- from typing import Any
1
+ from typing import Any, Sequence
3
2
 
4
- from qtpy.QtGui import QIcon, QKeySequence, QPixmap
5
- from qtpy.QtWidgets import *
3
+ from PySide6.QtCore import QSize
4
+ from PySide6.QtGui import QAction, QIcon, QKeySequence, QPixmap, QFont
5
+ from PySide6.QtWidgets import (
6
+ QMenu,
7
+ QSizePolicy,
8
+ QLabel,
9
+ QWidget,
10
+ QFormLayout,
11
+ QDockWidget,
12
+ QLayout,
13
+ QHBoxLayout,
14
+ QVBoxLayout,
15
+ )
6
16
 
7
- _logger = logging.getLogger(__name__)
8
17
 
18
+ def font(*, point_size: float | int | None = None, bold: bool | None = None) -> QFont:
19
+ _font = QFont()
9
20
 
10
- def make_truncatable(widget: QWidget) -> None:
11
- widget.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Fixed)
21
+ if point_size is not None:
22
+ if isinstance(point_size, int):
23
+ _font.setPointSize(point_size)
24
+ elif isinstance(point_size, float):
25
+ _font.setPointSizeF(point_size)
26
+ else:
27
+ raise TypeError("point_size")
28
+
29
+ if bold is not None:
30
+ _font.setBold(bold)
31
+
32
+ return _font
33
+
34
+
35
+ _bold = font(bold=True)
36
+
37
+
38
+ def label(text: str, *, font: QFont = None, allow_truncation: bool = False) -> QLabel:
39
+ _label = QLabel(text)
40
+
41
+ if font is not None:
42
+ _label.setFont(font)
43
+
44
+ if allow_truncation:
45
+ _label.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Fixed)
46
+
47
+ return _label
48
+
49
+
50
+ def bold_label(text: str, *, allow_truncation: bool = False) -> QLabel:
51
+ return label(text, allow_truncation=allow_truncation, font=_bold)
52
+
53
+
54
+ def form_layout(
55
+ rows: dict[str, QWidget], label_font: QFont | None = _bold, label_suffix: str = ":"
56
+ ) -> QFormLayout:
57
+ layout = QFormLayout()
58
+
59
+ for name, widget in rows.items():
60
+ if label_font:
61
+ layout.addRow(label(name + label_suffix, font=label_font), widget)
62
+ else:
63
+ layout.addRow(name + label_suffix, widget)
64
+
65
+ return layout
66
+
67
+
68
+ def size(width: int, height: int):
69
+ return QSize(width, height)
70
+
71
+
72
+ def dock_widget(
73
+ *,
74
+ widget: QWidget,
75
+ title: str | None = None,
76
+ hide: bool = False,
77
+ name: str | None = None,
78
+ min_size: QSize | None = None,
79
+ features: QDockWidget.DockWidgetFeature | None = None,
80
+ parent: QWidget | None = None,
81
+ ) -> QDockWidget:
82
+ dock = QDockWidget(title, parent)
83
+ dock.setWidget(widget)
84
+
85
+ if name is not None:
86
+ dock.setObjectName(name)
87
+
88
+ if min_size is not None:
89
+ dock.setMinimumSize(min_size)
90
+
91
+ if features is not None:
92
+ dock.setFeatures(features)
93
+
94
+ if hide:
95
+ dock.hide()
96
+
97
+ return dock
98
+
99
+
100
+ def widget(layout: QLayout) -> QWidget:
101
+ widget = QWidget()
102
+ widget.setLayout(layout)
103
+ return widget
104
+
105
+
106
+ def hbox(*items: QWidget | QLayout) -> QHBoxLayout:
107
+ _add_layout_items(layout := QHBoxLayout(), items)
108
+ return layout
109
+
110
+
111
+ def vbox(*items: QWidget | QLayout) -> QVBoxLayout:
112
+ _add_layout_items(layout := QVBoxLayout(), items)
113
+ return layout
114
+
115
+
116
+ def _add_layout_items(layout: QLayout, items: Sequence[QWidget | QLayout]):
117
+ for item in items:
118
+ if isinstance(item, QLayout):
119
+ layout.addItem(item)
120
+ else:
121
+ layout.addWidget(item)
12
122
 
13
123
 
14
124
  def create_qaction(
@@ -19,7 +129,7 @@ def create_qaction(
19
129
  triggered: Any = None,
20
130
  checkable: bool | None = None,
21
131
  checked: bool | None = None,
22
- icon: QIcon | QPixmap | None = None
132
+ icon: QIcon | QPixmap | None = None,
23
133
  ) -> QAction:
24
134
  action = QAction(name)
25
135
 
@@ -6,4 +6,4 @@ _logger = logging.getLogger(__name__)
6
6
 
7
7
 
8
8
  def is_url(url: str) -> bool:
9
- return validators.url(url) == True
9
+ return validators.url(url)
@@ -0,0 +1,95 @@
1
+ Metadata-Version: 2.4
2
+ Name: qcanvas
3
+ Version: 2026.1.19
4
+ Summary: View courses from Canvas LMS
5
+ Author: QCanvas
6
+ Author-email: QCanvas@noreply.codeberg.org
7
+ Requires-Python: >=3.12,<3.13
8
+ Classifier: Development Status :: 5 - Production/Stable
9
+ Classifier: Environment :: X11 Applications
10
+ Classifier: Environment :: X11 Applications :: Qt
11
+ Classifier: Framework :: Pydantic
12
+ Classifier: Framework :: Pydantic :: 2
13
+ Classifier: Intended Audience :: Education
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: License :: OSI Approved :: GNU Affero General Public License v3
16
+ Classifier: Natural Language :: English
17
+ Classifier: Operating System :: Microsoft :: Windows
18
+ Classifier: Operating System :: Microsoft :: Windows :: Windows 10
19
+ Classifier: Operating System :: Microsoft :: Windows :: Windows 11
20
+ Classifier: Operating System :: OS Independent
21
+ Classifier: Operating System :: POSIX :: Linux
22
+ Classifier: Programming Language :: Python
23
+ Classifier: Programming Language :: Python :: 3
24
+ Classifier: Programming Language :: Python :: 3.12
25
+ Classifier: Programming Language :: Python :: 3 :: Only
26
+ Classifier: Programming Language :: Python :: Implementation :: CPython
27
+ Classifier: Topic :: Database
28
+ Classifier: Topic :: Internet
29
+ Classifier: Topic :: Internet :: WWW/HTTP
30
+ Classifier: Topic :: Text Editors :: Text Processing
31
+ Classifier: Topic :: Text Processing :: Markup :: HTML
32
+ Classifier: Typing :: Typed
33
+ Requires-Dist: aiofile (>=3.9.0,<4.0.0)
34
+ Requires-Dist: aiosqlite (>=0.21.0,<0.22.0)
35
+ Requires-Dist: asynctaskpool (>=0.2.1,<0.3.0)
36
+ Requires-Dist: cachetools (>=6.2.4,<7.0.0)
37
+ Requires-Dist: libqcanvas (>=0.5.7,<0.6.0)
38
+ Requires-Dist: platformdirs (>=4.2.2,<5.0.0)
39
+ Requires-Dist: pyqtdarktheme-fork (>=2.3.2,<3.0.0)
40
+ Requires-Dist: qasync (>=0.28.0,<0.29.0)
41
+ Requires-Dist: sqlalchemy (>=2.0.45,<3.0.0)
42
+ Requires-Dist: validators (>=0.35.0,<0.36.0)
43
+ Description-Content-Type: text/markdown
44
+
45
+ # QCanvas
46
+
47
+ QCanvas is an **unofficial** desktop client for Canvas LMS.
48
+
49
+ https://codeberg.org/QCanvas/QCanvas
50
+
51
+ https://github.com/QCanvas/QCanvasApp
52
+
53
+ # Downloads
54
+
55
+ <a href='https://flathub.org/apps/io.github.qcanvas.QCanvasApp'>
56
+ <img width='240' alt='Get it on Flathub' src='https://flathub.org/api/badge?svg&locale=en'/>
57
+ </a>
58
+
59
+ You can download a **windows** version from [releases](https://github.com/QCanvas/QCanvasApp/releases)
60
+
61
+ An appimage version is also available from releases but is not recommended.
62
+
63
+ # Development/Run from source
64
+
65
+ ## Prerequisites
66
+
67
+ - Python 3.12
68
+ - Poetry
69
+
70
+ ## Get started
71
+
72
+ ```bash
73
+ git clone https://github.com/QCanvas/QCanvasApp.git
74
+ cd QCanvasApp
75
+
76
+ # Install packages and stuff
77
+ poetry install --with flatpak-exclude
78
+
79
+ # Run QCanvas (If you run `poetry shell`, you can drop the `poetry run` part)
80
+ poetry run qcanvas
81
+ # Alternative
82
+ poetry run python -m qcanvas
83
+ ```
84
+
85
+ ## Build custom AppImage
86
+
87
+ > [!WARNING]
88
+ > This is not recommended as the appimage produced by this process isn't a proper appimage. It's just a pyinstaller build bundled as an appimage.
89
+
90
+ > [!IMPORTANT]
91
+ > You will need [Appimagetool](https://github.com/AppImage/appimagetool)
92
+
93
+ ```bash
94
+ bash ./dev_scripts/build_appimage
95
+ ```