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.
- qcanvas/__init__.py +60 -0
- qcanvas/app.py +72 -0
- qcanvas/backend_connectors/frontend_resource_manager.py +13 -5
- qcanvas/backend_connectors/qcanvas_task_master.py +2 -2
- qcanvas/icons/__init__.py +5 -5
- qcanvas/icons/_icon_type.py +1 -1
- qcanvas/icons/icons.qrc +39 -35
- qcanvas/icons/rc_icons.py +1298 -1197
- qcanvas/settings/__init__.py +6 -0
- qcanvas/{util/settings → settings}/_client_settings.py +4 -4
- qcanvas/settings/_course_settings.py +54 -0
- qcanvas/{util/settings → settings}/_mapped_setting.py +2 -5
- qcanvas/{util/settings → settings}/_ui_settings.py +5 -5
- qcanvas/theme.py +101 -0
- qcanvas/ui/course_viewer/content_tree.py +9 -12
- qcanvas/ui/course_viewer/course_tree/_course_icon_generator.py +3 -3
- qcanvas/ui/course_viewer/course_tree/course_tree.py +9 -8
- qcanvas/ui/course_viewer/course_viewer.py +42 -56
- qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tab.py +107 -29
- qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +4 -4
- qcanvas/ui/course_viewer/tabs/constants.py +1 -0
- qcanvas/ui/course_viewer/tabs/content_tab.py +33 -39
- qcanvas/ui/course_viewer/tabs/file_tab/file_tab.py +4 -4
- qcanvas/ui/course_viewer/tabs/file_tab/file_tree.py +7 -10
- qcanvas/ui/course_viewer/tabs/file_tab/pages_file_tree.py +6 -7
- qcanvas/ui/course_viewer/tabs/mail_tab/mail_tab.py +50 -27
- qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +7 -8
- qcanvas/ui/course_viewer/tabs/page_tab/page_tab.py +3 -3
- qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +5 -5
- qcanvas/ui/course_viewer/tabs/resource_rich_browser.py +18 -32
- qcanvas/ui/course_viewer/tree_widget_data_item.py +1 -1
- qcanvas/ui/memory_tree/_tree_memory.py +45 -42
- qcanvas/ui/memory_tree/memory_tree_widget.py +22 -18
- qcanvas/ui/memory_tree/memory_tree_widget_item.py +3 -3
- qcanvas/ui/qcanvas_window/__init__.py +1 -0
- qcanvas/ui/{main_ui → qcanvas_window}/course_viewer_container.py +10 -10
- qcanvas/ui/{main_ui → qcanvas_window}/options/auto_download_resources_option.py +5 -5
- qcanvas/ui/{main_ui → qcanvas_window}/options/quick_sync_option.py +7 -6
- qcanvas/ui/{main_ui → qcanvas_window}/options/sync_on_start_option.py +7 -6
- qcanvas/ui/{main_ui → qcanvas_window}/options/theme_selection_menu.py +10 -10
- qcanvas/ui/{main_ui → qcanvas_window}/qcanvas_window.py +57 -41
- qcanvas/ui/{main_ui → qcanvas_window}/status_bar_progress_display.py +5 -6
- qcanvas/ui/qml_components/__init__.py +4 -0
- qcanvas/ui/qml_components/attachments_pane.py +70 -0
- qcanvas/ui/qml_components/comments_pane.py +83 -0
- qcanvas/ui/qml_components/qml/AttachmentsList.ui.qml +15 -0
- qcanvas/ui/qml_components/qml/AttachmentsListDelegate.qml +77 -0
- qcanvas/ui/qml_components/qml/AttachmentsListModel.qml +19 -0
- qcanvas/ui/qml_components/qml/AttachmentsPane.qml +11 -0
- qcanvas/ui/qml_components/qml/CommentsList.ui.qml +15 -0
- qcanvas/ui/qml_components/qml/CommentsListDelegate.ui.qml +118 -0
- qcanvas/ui/qml_components/qml/CommentsListModel.qml +56 -0
- qcanvas/ui/qml_components/qml/CommentsPane.qml +11 -0
- qcanvas/ui/qml_components/qml/DecoratedText.ui.qml +44 -0
- qcanvas/ui/qml_components/qml/Spacer.ui.qml +7 -0
- qcanvas/ui/qml_components/qml/ThemedRectangle.qml +53 -0
- qcanvas/ui/qml_components/qml/__init__.py +3 -0
- qcanvas/ui/qml_components/qml/rc_qml.py +709 -0
- qcanvas/ui/qml_components/qml/rc_qml.qrc +16 -0
- qcanvas/ui/qml_components/qml_bridge_types.py +95 -0
- qcanvas/ui/qml_components/qml_pane.py +21 -0
- qcanvas/ui/setup/setup_checker.py +1 -1
- qcanvas/ui/setup/setup_dialog.py +28 -14
- qcanvas/util/auto_downloader.py +9 -7
- qcanvas/util/basic_fonts.py +2 -2
- qcanvas/util/context_dict.py +12 -0
- qcanvas/util/file_icons.py +11 -19
- qcanvas/util/layouts.py +5 -7
- qcanvas/util/paths.py +17 -6
- qcanvas/util/qurl_util.py +1 -1
- qcanvas/util/ui_tools.py +118 -8
- qcanvas/util/url_checker.py +1 -1
- qcanvas-2026.1.19.dist-info/METADATA +95 -0
- qcanvas-2026.1.19.dist-info/RECORD +92 -0
- {qcanvas-1.2.0.dist-info → qcanvas-2026.1.19.dist-info}/WHEEL +1 -1
- qcanvas-2026.1.19.dist-info/entry_points.txt +3 -0
- qcanvas/app_start/__init__.py +0 -59
- qcanvas/icons/_update_icons.py +0 -89
- qcanvas/icons/dark/actions/exit.svg +0 -3
- qcanvas/icons/dark/actions/mark_all_read.svg +0 -3
- qcanvas/icons/dark/actions/open_downloads.svg +0 -3
- qcanvas/icons/dark/actions/quick_login.svg +0 -3
- qcanvas/icons/dark/actions/sync.svg +0 -3
- qcanvas/icons/dark/branding/logo_transparent.svg +0 -303
- qcanvas/icons/dark/options/auto_download.svg +0 -3
- qcanvas/icons/dark/options/theme.svg +0 -3
- qcanvas/icons/dark/tabs/assignments.svg +0 -3
- qcanvas/icons/dark/tabs/mail.svg +0 -3
- qcanvas/icons/dark/tabs/pages.svg +0 -3
- qcanvas/icons/dark/tree_items/assignment.svg +0 -3
- qcanvas/icons/dark/tree_items/mail.svg +0 -3
- qcanvas/icons/dark/tree_items/module.svg +0 -3
- qcanvas/icons/dark/tree_items/page.svg +0 -3
- qcanvas/icons/light/actions/exit.svg +0 -3
- qcanvas/icons/light/actions/mark_all_read.svg +0 -3
- qcanvas/icons/light/actions/open_downloads.svg +0 -3
- qcanvas/icons/light/actions/quick_login.svg +0 -3
- qcanvas/icons/light/actions/sync.svg +0 -3
- qcanvas/icons/light/branding/logo_transparent.svg +0 -304
- qcanvas/icons/light/options/auto_download.svg +0 -3
- qcanvas/icons/light/options/ignore_old.svg +0 -3
- qcanvas/icons/light/options/include_videos.svg +0 -3
- qcanvas/icons/light/options/theme.svg +0 -3
- qcanvas/icons/light/tabs/assignments.svg +0 -3
- qcanvas/icons/light/tabs/mail.svg +0 -3
- qcanvas/icons/light/tabs/pages.svg +0 -3
- qcanvas/icons/light/tree_items/assignment.svg +0 -3
- qcanvas/icons/light/tree_items/mail.svg +0 -3
- qcanvas/icons/light/tree_items/module.svg +0 -3
- qcanvas/icons/light/tree_items/page.svg +0 -3
- qcanvas/icons/universal/branding/main_icon.svg +0 -325
- qcanvas/icons/universal/downloads/download_failed.svg +0 -23
- qcanvas/icons/universal/downloads/downloaded.svg +0 -23
- qcanvas/icons/universal/downloads/not_downloaded.svg +0 -23
- qcanvas/icons/universal/downloads/unknown.svg +0 -6
- qcanvas/icons/universal/tabs/assignments_new_content.svg +0 -3
- qcanvas/icons/universal/tabs/mail_new_content.svg +0 -3
- qcanvas/icons/universal/tabs/pages_new_content.svg +0 -3
- qcanvas/icons/universal/tree_items/semester.svg +0 -108
- qcanvas/run.py +0 -54
- qcanvas/ui/course_viewer/tabs/util.py +0 -11
- qcanvas/ui/main_ui/__init__.py +0 -0
- qcanvas/util/settings/__init__.py +0 -9
- qcanvas/util/themes/__init__.py +0 -2
- qcanvas/util/themes/_colour_scheme_helper.py +0 -38
- qcanvas/util/themes/_selected_theme.py +0 -10
- qcanvas/util/themes/_theme_changed_event.py +0 -17
- qcanvas/util/themes/_theme_changer.py +0 -86
- qcanvas-1.2.0.dist-info/METADATA +0 -71
- qcanvas-1.2.0.dist-info/RECORD +0 -118
- qcanvas-1.2.0.dist-info/entry_points.txt +0 -3
- /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)
|
qcanvas/ui/setup/setup_dialog.py
CHANGED
|
@@ -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
|
-
|
|
7
|
-
|
|
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(
|
|
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.
|
|
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(
|
|
303
|
+
msg.accepted.connect(
|
|
304
|
+
self._open_tutorial, Qt.ConnectionType.SingleShotConnection
|
|
305
|
+
)
|
|
292
306
|
msg.show()
|
|
293
307
|
|
|
294
308
|
@Slot()
|
qcanvas/util/auto_downloader.py
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
from
|
|
6
|
-
from
|
|
7
|
-
from
|
|
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
|
-
|
|
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.
|
|
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)
|
qcanvas/util/basic_fonts.py
CHANGED
|
@@ -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)
|
qcanvas/util/file_icons.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os.path
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
from
|
|
6
|
-
from
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
5
|
-
from
|
|
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:
|
|
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:
|
|
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
|
|
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
qcanvas/util/ui_tools.py
CHANGED
|
@@ -1,14 +1,124 @@
|
|
|
1
|
-
import
|
|
2
|
-
from typing import Any
|
|
1
|
+
from typing import Any, Sequence
|
|
3
2
|
|
|
4
|
-
from
|
|
5
|
-
from
|
|
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
|
-
|
|
11
|
-
|
|
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
|
|
qcanvas/util/url_checker.py
CHANGED
|
@@ -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
|
+
```
|