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
|
@@ -1,29 +1,39 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from threading import BoundedSemaphore
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Optional, Sequence
|
|
4
4
|
|
|
5
5
|
import httpx
|
|
6
|
-
|
|
6
|
+
from libqcanvas import db
|
|
7
|
+
from libqcanvas.database.data_monolith import DataMonolith
|
|
8
|
+
from libqcanvas.net.sync.sync_receipt import SyncReceipt, empty_receipt
|
|
9
|
+
from libqcanvas.qcanvas import QCanvas
|
|
10
|
+
from PySide6.QtCore import QUrl, Signal, Slot, Qt
|
|
11
|
+
from PySide6.QtGui import QDesktopServices, QKeySequence
|
|
12
|
+
from PySide6.QtWidgets import (
|
|
13
|
+
QErrorMessage,
|
|
14
|
+
QHBoxLayout,
|
|
15
|
+
QMainWindow,
|
|
16
|
+
QProgressDialog,
|
|
17
|
+
QPushButton,
|
|
18
|
+
QVBoxLayout,
|
|
19
|
+
QWidget,
|
|
20
|
+
)
|
|
7
21
|
from qasync import asyncSlot
|
|
8
|
-
from qcanvas_backend.database.data_monolith import DataMonolith
|
|
9
|
-
from qcanvas_backend.net.sync.sync_receipt import SyncReceipt, empty_receipt
|
|
10
|
-
from qcanvas_backend.qcanvas import QCanvas
|
|
11
|
-
from qtpy.QtCore import QUrl, Signal, Slot
|
|
12
|
-
from qtpy.QtGui import QDesktopServices, QKeySequence
|
|
13
|
-
from qtpy.QtWidgets import *
|
|
14
22
|
|
|
15
23
|
from qcanvas import icons
|
|
16
24
|
from qcanvas.backend_connectors import FrontendResourceManager
|
|
25
|
+
from qcanvas.settings import course_configs
|
|
17
26
|
from qcanvas.ui.course_viewer import CourseTree
|
|
18
|
-
from
|
|
19
|
-
from
|
|
27
|
+
from .course_viewer_container import CourseViewerContainer
|
|
28
|
+
from .options.auto_download_resources_option import (
|
|
20
29
|
AutoDownloadResourcesMenu,
|
|
21
30
|
)
|
|
22
|
-
from
|
|
23
|
-
from
|
|
24
|
-
from
|
|
25
|
-
from
|
|
26
|
-
from qcanvas.util import auto_downloader
|
|
31
|
+
from .options.quick_sync_option import QuickSyncOption
|
|
32
|
+
from .options.sync_on_start_option import SyncOnStartOption
|
|
33
|
+
from .options.theme_selection_menu import ThemeSelectionMenu
|
|
34
|
+
from .status_bar_progress_display import StatusBarProgressDisplay
|
|
35
|
+
from qcanvas.util import auto_downloader
|
|
36
|
+
import qcanvas.settings as settings
|
|
27
37
|
from qcanvas.util.qurl_util import file_url
|
|
28
38
|
from qcanvas.util.ui_tools import create_qaction
|
|
29
39
|
|
|
@@ -33,7 +43,7 @@ _logger = logging.getLogger(__name__)
|
|
|
33
43
|
class QCanvasWindow(QMainWindow):
|
|
34
44
|
_loaded = Signal()
|
|
35
45
|
|
|
36
|
-
def __init__(self):
|
|
46
|
+
def __init__(self, _qcanvas: QCanvas[FrontendResourceManager]):
|
|
37
47
|
super().__init__()
|
|
38
48
|
|
|
39
49
|
self.setWindowTitle("QCanvas")
|
|
@@ -41,12 +51,7 @@ class QCanvasWindow(QMainWindow):
|
|
|
41
51
|
|
|
42
52
|
self._operation_semaphore = BoundedSemaphore()
|
|
43
53
|
self._data: Optional[DataMonolith] = None
|
|
44
|
-
self._qcanvas =
|
|
45
|
-
canvas_config=settings.client.canvas_config,
|
|
46
|
-
panopto_config=settings.client.panopto_config,
|
|
47
|
-
storage_path=paths.data_storage(),
|
|
48
|
-
resource_manager_class=FrontendResourceManager,
|
|
49
|
-
)
|
|
54
|
+
self._qcanvas = _qcanvas
|
|
50
55
|
|
|
51
56
|
self._course_tree = CourseTree()
|
|
52
57
|
self._course_tree.item_selected.connect(self._on_course_selected)
|
|
@@ -62,7 +67,9 @@ class QCanvasWindow(QMainWindow):
|
|
|
62
67
|
self._setup_menu_bar()
|
|
63
68
|
self._restore_window_position()
|
|
64
69
|
|
|
65
|
-
self._loaded.connect(
|
|
70
|
+
self._loaded.connect(
|
|
71
|
+
self._on_app_loaded, Qt.ConnectionType.SingleShotConnection
|
|
72
|
+
)
|
|
66
73
|
self._loaded.emit()
|
|
67
74
|
|
|
68
75
|
def _setup_menu_bar(self) -> None:
|
|
@@ -109,7 +116,7 @@ class QCanvasWindow(QMainWindow):
|
|
|
109
116
|
)
|
|
110
117
|
|
|
111
118
|
options_menu = menu_bar.addMenu("Options")
|
|
112
|
-
|
|
119
|
+
options_menu.setToolTipsVisible(True)
|
|
113
120
|
options_menu.addAction(QuickSyncOption(options_menu))
|
|
114
121
|
options_menu.addAction(SyncOnStartOption(options_menu))
|
|
115
122
|
options_menu.addMenu(AutoDownloadResourcesMenu(options_menu))
|
|
@@ -147,7 +154,6 @@ class QCanvasWindow(QMainWindow):
|
|
|
147
154
|
|
|
148
155
|
@asyncSlot()
|
|
149
156
|
async def _on_app_loaded(self) -> None:
|
|
150
|
-
await self._qcanvas.init()
|
|
151
157
|
self._course_tree.reload(await self._get_terms(), sync_receipt=empty_receipt())
|
|
152
158
|
|
|
153
159
|
if settings.client.sync_on_start:
|
|
@@ -171,12 +177,18 @@ class QCanvasWindow(QMainWindow):
|
|
|
171
177
|
except Exception as e:
|
|
172
178
|
_logger.warning("Sync failed", exc_info=e)
|
|
173
179
|
error = QErrorMessage(self)
|
|
174
|
-
msg = str(e)
|
|
175
180
|
|
|
176
|
-
if isinstance(e,
|
|
177
|
-
msg = "
|
|
181
|
+
if isinstance(e, ExceptionGroup):
|
|
182
|
+
msg = "\n".join(str(ex) for ex in e.exceptions)
|
|
183
|
+
elif isinstance(e, httpx.ConnectError):
|
|
184
|
+
msg = "You may not be connected to the Internet"
|
|
185
|
+
else:
|
|
186
|
+
msg = str(e)
|
|
178
187
|
|
|
179
|
-
error.
|
|
188
|
+
error.setWindowTitle("An error has occurred in QCanvas")
|
|
189
|
+
error.showMessage(
|
|
190
|
+
msg + " - Please check the log for more details", "sync error"
|
|
191
|
+
)
|
|
180
192
|
finally:
|
|
181
193
|
self._operation_semaphore.release()
|
|
182
194
|
self._sync_button.setText("Synchronise")
|
|
@@ -199,14 +211,14 @@ class QCanvasWindow(QMainWindow):
|
|
|
199
211
|
await self._get_courses(), sync_receipt=receipt
|
|
200
212
|
)
|
|
201
213
|
|
|
202
|
-
async def _get_resources(self) ->
|
|
203
|
-
return (await self._qcanvas.
|
|
214
|
+
async def _get_resources(self) -> dict[str, db.Resource]:
|
|
215
|
+
return (await self._qcanvas.load()).resources
|
|
204
216
|
|
|
205
217
|
async def _get_terms(self) -> Sequence[db.Term]:
|
|
206
|
-
return (await self._qcanvas.
|
|
218
|
+
return (await self._qcanvas.load()).terms
|
|
207
219
|
|
|
208
220
|
async def _get_courses(self) -> Sequence[db.Course]:
|
|
209
|
-
return (await self._qcanvas.
|
|
221
|
+
return (await self._qcanvas.load()).courses
|
|
210
222
|
|
|
211
223
|
@Slot(db.Course)
|
|
212
224
|
def _on_course_selected(self, course: Optional[db.Course]) -> None:
|
|
@@ -218,20 +230,24 @@ class QCanvasWindow(QMainWindow):
|
|
|
218
230
|
@asyncSlot(db.Course, str)
|
|
219
231
|
async def _on_course_renamed(self, course: db.Course, new_name: str) -> None:
|
|
220
232
|
_logger.debug("Rename %s -> %s", course.name, new_name)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
course.configuration.nickname = new_name
|
|
233
|
+
config = course_configs[course.id]
|
|
234
|
+
config.nickname = new_name
|
|
235
|
+
await config.save()
|
|
225
236
|
|
|
226
237
|
@asyncSlot()
|
|
227
238
|
async def _open_quick_auth_in_browser(self) -> None:
|
|
228
239
|
opening_progress_dialog = QProgressDialog("Opening canvas", None, 0, 0, self)
|
|
229
240
|
opening_progress_dialog.setWindowTitle("Please wait")
|
|
230
241
|
opening_progress_dialog.show()
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
open_url = QUrl(
|
|
245
|
+
await self._qcanvas.canvas_client.get_temporary_session_url()
|
|
246
|
+
)
|
|
247
|
+
_logger.info(f"Opening URL {open_url}")
|
|
248
|
+
QDesktopServices.openUrl(open_url)
|
|
249
|
+
finally:
|
|
250
|
+
opening_progress_dialog.close()
|
|
235
251
|
|
|
236
252
|
@Slot()
|
|
237
253
|
def _open_downloads_folder(self) -> None:
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from asyncio import Lock
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from typing import *
|
|
5
4
|
|
|
5
|
+
from libqcanvas.task_master import TaskID
|
|
6
|
+
from PySide6.QtWidgets import QProgressBar, QStatusBar
|
|
6
7
|
from qasync import asyncSlot
|
|
7
|
-
from qcanvas_backend.task_master import TaskID
|
|
8
|
-
from qtpy.QtWidgets import *
|
|
9
8
|
|
|
10
9
|
from qcanvas.backend_connectors import task_master
|
|
11
10
|
|
|
@@ -90,7 +89,7 @@ class StatusBarProgressDisplay(QStatusBar):
|
|
|
90
89
|
self.showMessage("Done", 5000)
|
|
91
90
|
self._progress_bar.hide()
|
|
92
91
|
|
|
93
|
-
def _show_single_task_progress(self, task:
|
|
92
|
+
def _show_single_task_progress(self, task: tuple[TaskID, _TaskProgress]) -> None:
|
|
94
93
|
_logger.debug("Single task %s", task)
|
|
95
94
|
id, progress = task
|
|
96
95
|
|
|
@@ -98,7 +97,7 @@ class StatusBarProgressDisplay(QStatusBar):
|
|
|
98
97
|
self.showMessage(id.step_name)
|
|
99
98
|
|
|
100
99
|
def _show_multiple_tasks_progress(
|
|
101
|
-
self, tasks: list[
|
|
100
|
+
self, tasks: list[tuple[TaskID, _TaskProgress]]
|
|
102
101
|
) -> None:
|
|
103
102
|
_logger.debug("Multiple tasks %s", tasks)
|
|
104
103
|
self.showMessage(
|
|
@@ -107,7 +106,7 @@ class StatusBarProgressDisplay(QStatusBar):
|
|
|
107
106
|
self._show_progress(self._calculate_progress(tasks))
|
|
108
107
|
|
|
109
108
|
def _calculate_progress(
|
|
110
|
-
self, tasks: list[
|
|
109
|
+
self, tasks: list[tuple[TaskID, _TaskProgress]]
|
|
111
110
|
) -> _TaskProgress:
|
|
112
111
|
# Task progresses are floats from 0 to 1, multiplier is used to turn them into ints
|
|
113
112
|
multiplier = 1000
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from PySide6.QtCore import Slot, QUrl
|
|
2
|
+
from PySide6.QtWidgets import QWidget
|
|
3
|
+
from qasync import asyncSlot
|
|
4
|
+
|
|
5
|
+
from qcanvas.backend_connectors import FrontendResourceManager
|
|
6
|
+
from .qml_bridge_types import Attachment
|
|
7
|
+
from .qml_pane import QmlPane
|
|
8
|
+
from libqcanvas import db
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
_logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AttachmentsPane(QmlPane):
|
|
15
|
+
def __init__(
|
|
16
|
+
self, downloader: FrontendResourceManager, parent: QWidget | None = None
|
|
17
|
+
):
|
|
18
|
+
super().__init__(qml_path=QUrl("qrc:/qml/AttachmentsPane.qml"), parent=parent)
|
|
19
|
+
|
|
20
|
+
self._original_dock_name = None
|
|
21
|
+
self._downloader = downloader
|
|
22
|
+
self._files: dict[str, db.Resource] = {}
|
|
23
|
+
self._qfiles: dict[str, Attachment] = {}
|
|
24
|
+
self.ctx["submission_files"] = []
|
|
25
|
+
self.load_view()
|
|
26
|
+
|
|
27
|
+
self._downloader.download_finished.connect(self._download_updated)
|
|
28
|
+
self._downloader.download_failed.connect(self._download_updated)
|
|
29
|
+
|
|
30
|
+
def clear_files(self):
|
|
31
|
+
self.ctx["submission_files"] = []
|
|
32
|
+
self._files.clear()
|
|
33
|
+
self._qfiles.clear()
|
|
34
|
+
|
|
35
|
+
def load_files(self, files: list[db.Resource]):
|
|
36
|
+
qfiles = []
|
|
37
|
+
|
|
38
|
+
if self._original_dock_name is None:
|
|
39
|
+
self._original_dock_name = self.parent().windowTitle()
|
|
40
|
+
|
|
41
|
+
self.parent().setWindowTitle(f"{self._original_dock_name} ({len(files)})")
|
|
42
|
+
|
|
43
|
+
for file in files:
|
|
44
|
+
qfile = Attachment(
|
|
45
|
+
file_name=file.file_name,
|
|
46
|
+
resource_id=file.id,
|
|
47
|
+
download_state=file.download_state,
|
|
48
|
+
)
|
|
49
|
+
qfile.opened.connect(self._on_attachment_opened)
|
|
50
|
+
qfiles.append(qfile)
|
|
51
|
+
|
|
52
|
+
self._qfiles[file.id] = qfile
|
|
53
|
+
self._files[file.id] = file
|
|
54
|
+
|
|
55
|
+
self.ctx["submission_files"] = qfiles
|
|
56
|
+
|
|
57
|
+
@asyncSlot(str)
|
|
58
|
+
async def _on_attachment_opened(self, resource_id: str) -> None:
|
|
59
|
+
if resource_id in self._files:
|
|
60
|
+
await self._downloader.download_and_open(self._files[resource_id])
|
|
61
|
+
else:
|
|
62
|
+
_logger.warning(
|
|
63
|
+
"User opened an attachment that doesn't belong to any comment! id=%s",
|
|
64
|
+
resource_id,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@Slot(db.Resource)
|
|
68
|
+
def _download_updated(self, resource: db.Resource) -> None:
|
|
69
|
+
if resource.id in self._files:
|
|
70
|
+
self._qfiles[resource.id].download_state = resource.download_state
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from PySide6.QtCore import Signal, Slot, QUrl
|
|
2
|
+
from PySide6.QtWidgets import QWidget
|
|
3
|
+
from libqcanvas.util import remove_unwanted_whitespaces, as_local
|
|
4
|
+
from qasync import asyncSlot
|
|
5
|
+
|
|
6
|
+
from qcanvas.backend_connectors import FrontendResourceManager
|
|
7
|
+
from .qml_bridge_types import Attachment, Comment
|
|
8
|
+
from libqcanvas import db
|
|
9
|
+
import logging
|
|
10
|
+
from .qml_pane import QmlPane
|
|
11
|
+
|
|
12
|
+
_logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CommentsPane(QmlPane):
|
|
16
|
+
attachment_opened = Signal(str)
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self, downloader: FrontendResourceManager, parent: QWidget | None = None
|
|
20
|
+
):
|
|
21
|
+
super().__init__(qml_path=QUrl("qrc:/qml/CommentsPane.qml"), parent=parent)
|
|
22
|
+
self._downloader = downloader
|
|
23
|
+
self._attachments: dict[str, db.Resource] = {}
|
|
24
|
+
self._qattachments: dict[str, Attachment] = {}
|
|
25
|
+
|
|
26
|
+
# Add context objects before we load the view
|
|
27
|
+
self.ctx["comments"] = []
|
|
28
|
+
self.load_view()
|
|
29
|
+
|
|
30
|
+
self._downloader.download_finished.connect(self._download_updated)
|
|
31
|
+
self._downloader.download_failed.connect(self._download_updated)
|
|
32
|
+
|
|
33
|
+
def clear_comments(self) -> None:
|
|
34
|
+
self.ctx["comments"] = []
|
|
35
|
+
self._attachments.clear()
|
|
36
|
+
self._qattachments.clear()
|
|
37
|
+
|
|
38
|
+
def load_comments(self, comments: list[db.SubmissionComment]) -> None:
|
|
39
|
+
qcomments = []
|
|
40
|
+
|
|
41
|
+
self.parent().setWindowTitle(f"Comments ({len(comments)})")
|
|
42
|
+
|
|
43
|
+
for comment in comments:
|
|
44
|
+
attachments = []
|
|
45
|
+
|
|
46
|
+
for attachment in comment.attachments:
|
|
47
|
+
qattachment = Attachment(
|
|
48
|
+
file_name=attachment.file_name,
|
|
49
|
+
resource_id=attachment.id,
|
|
50
|
+
download_state=attachment.download_state,
|
|
51
|
+
)
|
|
52
|
+
qattachment.opened.connect(self._on_attachment_opened)
|
|
53
|
+
attachments.append(qattachment)
|
|
54
|
+
|
|
55
|
+
self._attachments[attachment.id] = attachment
|
|
56
|
+
self._qattachments[attachment.id] = qattachment
|
|
57
|
+
|
|
58
|
+
qcomments.append(
|
|
59
|
+
Comment(
|
|
60
|
+
body=remove_unwanted_whitespaces(comment.body),
|
|
61
|
+
author=comment.author,
|
|
62
|
+
date=as_local(comment.creation_date).strftime("%Y-%m-%d %H:%M"),
|
|
63
|
+
attachments=attachments,
|
|
64
|
+
parent=self,
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
self.ctx["comments"] = qcomments
|
|
69
|
+
|
|
70
|
+
@asyncSlot(str)
|
|
71
|
+
async def _on_attachment_opened(self, resource_id: str) -> None:
|
|
72
|
+
if resource_id in self._attachments:
|
|
73
|
+
await self._downloader.download_and_open(self._attachments[resource_id])
|
|
74
|
+
else:
|
|
75
|
+
_logger.warning(
|
|
76
|
+
"User opened an attachment that doesn't belong to any comment! id=%s",
|
|
77
|
+
resource_id,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
@Slot(db.Resource)
|
|
81
|
+
def _download_updated(self, resource: db.Resource) -> None:
|
|
82
|
+
if resource.id in self._qattachments:
|
|
83
|
+
self._qattachments[resource.id].download_state = resource.download_state
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
This is a UI file (.ui.qml) that is intended to be edited in Qt Design Studio only.
|
|
5
|
+
It is supposed to be strictly declarative and only uses a subset of QML. If you edit
|
|
6
|
+
this file manually, you might introduce QML code that is not supported by Qt Design Studio.
|
|
7
|
+
Check out https://doc.qt.io/qtcreator/creator-quick-ui-forms.html for details on .ui.qml files.
|
|
8
|
+
*/
|
|
9
|
+
import QtQuick
|
|
10
|
+
|
|
11
|
+
ListView {
|
|
12
|
+
id: view
|
|
13
|
+
model: AttachmentsListModel {}
|
|
14
|
+
delegate: AttachmentsListDelegate {}
|
|
15
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import QtQuick
|
|
2
|
+
import QtQuick.Layouts
|
|
3
|
+
|
|
4
|
+
Item {
|
|
5
|
+
id: delegate
|
|
6
|
+
height: childrenRect.height
|
|
7
|
+
anchors {
|
|
8
|
+
left: parent.left
|
|
9
|
+
right: parent.right
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
ColumnLayout {
|
|
13
|
+
spacing: 0
|
|
14
|
+
|
|
15
|
+
anchors {
|
|
16
|
+
left: parent.left
|
|
17
|
+
right: parent.right
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
RowLayout {
|
|
21
|
+
height: childrenRect.implicitHeight
|
|
22
|
+
Layout.fillWidth: true
|
|
23
|
+
spacing: 10
|
|
24
|
+
|
|
25
|
+
Image {
|
|
26
|
+
function attachmentIcon(downloadStatus) {
|
|
27
|
+
switch(downloadStatus)
|
|
28
|
+
{
|
|
29
|
+
case "DOWNLOADED":
|
|
30
|
+
return "qrc:///icons/universal/downloads/downloaded.svg";
|
|
31
|
+
case "FAILED":
|
|
32
|
+
return "qrc:///icons/universal/downloads/download_failed.svg";
|
|
33
|
+
case "NOT_DOWNLOADED":
|
|
34
|
+
return "qrc:///icons/universal/downloads/not_downloaded.svg";
|
|
35
|
+
default:
|
|
36
|
+
return "qrc:///icons/universal/downloads/unknown.svg"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
id: fileIcon
|
|
41
|
+
source: attachmentIcon(modelData.download_state)
|
|
42
|
+
fillMode: Image.PreserveAspectFit
|
|
43
|
+
sourceSize.height: 17
|
|
44
|
+
sourceSize.width: 17
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
Text {
|
|
48
|
+
id: file_name_text
|
|
49
|
+
text: modelData.file_name
|
|
50
|
+
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
|
51
|
+
Layout.fillWidth: true
|
|
52
|
+
font.underline: true
|
|
53
|
+
color: palette.link
|
|
54
|
+
|
|
55
|
+
MouseArea {
|
|
56
|
+
id: textClickArea
|
|
57
|
+
anchors.fill: parent
|
|
58
|
+
cursorShape: Qt.PointingHandCursor
|
|
59
|
+
hoverEnabled: true
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
Spacer {
|
|
65
|
+
size: 5
|
|
66
|
+
visible: index !== count - 1
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
Connections {
|
|
71
|
+
target: textClickArea
|
|
72
|
+
|
|
73
|
+
function onClicked() {
|
|
74
|
+
modelData.opened(modelData.resource_id)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import QtQuick
|
|
2
|
+
|
|
3
|
+
ListModel {
|
|
4
|
+
ListElement {
|
|
5
|
+
file_name: "texas.pdf"
|
|
6
|
+
resource_id: "1"
|
|
7
|
+
download_state: "NOT_DOWNLOADED"
|
|
8
|
+
}
|
|
9
|
+
ListElement {
|
|
10
|
+
file_name: "oh_no_what_a_terribly_long_file_name_its_not_like_someone_would_actually_do_this.pdf"
|
|
11
|
+
resource_id: "2"
|
|
12
|
+
download_state: "FAILED"
|
|
13
|
+
}
|
|
14
|
+
ListElement {
|
|
15
|
+
file_name: "i was transported to another world where javascript doesn't exist.cbz"
|
|
16
|
+
resource_id: "3"
|
|
17
|
+
download_state: "DOWNLOADED"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
This is a UI file (.ui.qml) that is intended to be edited in Qt Design Studio only.
|
|
5
|
+
It is supposed to be strictly declarative and only uses a subset of QML. If you edit
|
|
6
|
+
this file manually, you might introduce QML code that is not supported by Qt Design Studio.
|
|
7
|
+
Check out https://doc.qt.io/qtcreator/creator-quick-ui-forms.html for details on .ui.qml files.
|
|
8
|
+
*/
|
|
9
|
+
import QtQuick
|
|
10
|
+
|
|
11
|
+
ListView {
|
|
12
|
+
id: view
|
|
13
|
+
model: CommentsListModel {}
|
|
14
|
+
delegate: CommentsListDelegate {}
|
|
15
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
This is a UI file (.ui.qml) that is intended to be edited in Qt Design Studio only.
|
|
5
|
+
It is supposed to be strictly declarative and only uses a subset of QML. If you edit
|
|
6
|
+
this file manually, you might introduce QML code that is not supported by Qt Design Studio.
|
|
7
|
+
Check out https://doc.qt.io/qtcreator/creator-quick-ui-forms.html for details on .ui.qml files.
|
|
8
|
+
*/
|
|
9
|
+
import QtQuick
|
|
10
|
+
import QtQuick.Controls
|
|
11
|
+
|
|
12
|
+
Control {
|
|
13
|
+
padding: 15
|
|
14
|
+
topInset: 5
|
|
15
|
+
bottomInset: 5
|
|
16
|
+
background: Rectangle {
|
|
17
|
+
color: palette.midlight
|
|
18
|
+
radius: 4
|
|
19
|
+
border.color: palette.dark
|
|
20
|
+
}
|
|
21
|
+
clip: true
|
|
22
|
+
height: delegate.height + padding * 2
|
|
23
|
+
anchors {
|
|
24
|
+
left: view.contentItem.left
|
|
25
|
+
right: view.contentItem.right
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
contentItem: Item {
|
|
29
|
+
id: delegate
|
|
30
|
+
height: column.height
|
|
31
|
+
anchors {
|
|
32
|
+
left: parent.left
|
|
33
|
+
right: parent.right
|
|
34
|
+
margins: padding
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
Column {
|
|
38
|
+
id: column
|
|
39
|
+
anchors {
|
|
40
|
+
left: parent.left
|
|
41
|
+
right: parent.right
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
Item {
|
|
45
|
+
height: authorText.height
|
|
46
|
+
anchors {
|
|
47
|
+
left: parent.left
|
|
48
|
+
right: parent.right
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
Text {
|
|
52
|
+
id: authorText
|
|
53
|
+
text: modelData.author
|
|
54
|
+
clip: true
|
|
55
|
+
color: palette.text
|
|
56
|
+
|
|
57
|
+
font {
|
|
58
|
+
pointSize: 12
|
|
59
|
+
bold: true
|
|
60
|
+
}
|
|
61
|
+
anchors {
|
|
62
|
+
left: parent.left
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
Text {
|
|
67
|
+
id: commentDate
|
|
68
|
+
text: modelData.date
|
|
69
|
+
verticalAlignment: Text.AlignVCenter
|
|
70
|
+
horizontalAlignment: Text.AlignRight
|
|
71
|
+
color: palette.text
|
|
72
|
+
clip: true
|
|
73
|
+
|
|
74
|
+
anchors {
|
|
75
|
+
left: authorText.right
|
|
76
|
+
right: parent.right
|
|
77
|
+
top: parent.top
|
|
78
|
+
bottom: parent.bottom
|
|
79
|
+
leftMargin: 5
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
Spacer {
|
|
85
|
+
size: 10
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
DecoratedText {
|
|
89
|
+
text: modelData.body
|
|
90
|
+
lineWidth: 2
|
|
91
|
+
anchors {
|
|
92
|
+
left: parent.left
|
|
93
|
+
right: parent.right
|
|
94
|
+
}
|
|
95
|
+
content {
|
|
96
|
+
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
Spacer {
|
|
101
|
+
size: 10
|
|
102
|
+
visible: modelData.attachments.length > 0
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
AttachmentsList {
|
|
106
|
+
id: attachmentsList
|
|
107
|
+
height: contentHeight
|
|
108
|
+
model: modelData.attachments
|
|
109
|
+
interactive: false
|
|
110
|
+
visible: modelData.attachments.length > 0
|
|
111
|
+
anchors {
|
|
112
|
+
left: parent.left
|
|
113
|
+
right: parent.right
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|