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.
- 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 +55 -6
- qcanvas/icons/_icon_type.py +42 -0
- qcanvas/icons/icons.qrc +48 -8
- qcanvas/icons/rc_icons.py +2477 -566
- qcanvas/settings/__init__.py +6 -0
- qcanvas/{util/settings → settings}/_client_settings.py +15 -6
- qcanvas/settings/_course_settings.py +54 -0
- qcanvas/{util/settings → settings}/_mapped_setting.py +8 -6
- qcanvas/{util/settings → settings}/_ui_settings.py +5 -5
- qcanvas/theme.py +101 -0
- qcanvas/ui/course_viewer/content_tree.py +37 -19
- qcanvas/ui/course_viewer/course_tree/__init__.py +1 -0
- qcanvas/ui/course_viewer/course_tree/_course_icon_generator.py +86 -0
- qcanvas/ui/course_viewer/{course_tree.py → course_tree/course_tree.py} +29 -14
- qcanvas/ui/course_viewer/course_viewer.py +79 -46
- qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tab.py +107 -29
- qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +19 -18
- qcanvas/ui/course_viewer/tabs/content_tab.py +33 -39
- qcanvas/ui/course_viewer/tabs/file_tab/__init__.py +1 -0
- qcanvas/ui/course_viewer/tabs/file_tab/file_tab.py +46 -0
- qcanvas/ui/course_viewer/tabs/file_tab/file_tree.py +96 -0
- qcanvas/ui/course_viewer/tabs/file_tab/pages_file_tree.py +55 -0
- qcanvas/ui/course_viewer/tabs/mail_tab/mail_tab.py +50 -27
- qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +18 -19
- qcanvas/ui/course_viewer/tabs/page_tab/page_tab.py +3 -3
- qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +18 -16
- qcanvas/ui/course_viewer/tabs/resource_rich_browser.py +61 -74
- qcanvas/ui/course_viewer/tree_widget_data_item.py +22 -0
- qcanvas/ui/memory_tree/_tree_memory.py +45 -41
- 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/qcanvas_window/course_viewer_container.py +95 -0
- qcanvas/ui/{main_ui → qcanvas_window}/options/auto_download_resources_option.py +8 -6
- 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 +12 -10
- qcanvas/ui/{main_ui → qcanvas_window}/qcanvas_window.py +74 -45
- qcanvas/ui/{main_ui → qcanvas_window}/status_bar_progress_display.py +20 -12
- 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 +3 -3
- qcanvas/ui/setup/setup_dialog.py +173 -80
- qcanvas/util/__init__.py +0 -2
- qcanvas/util/auto_downloader.py +9 -8
- qcanvas/util/basic_fonts.py +2 -2
- qcanvas/util/context_dict.py +12 -0
- qcanvas/util/file_icons.py +46 -0
- qcanvas/util/html_cleaner.py +2 -0
- qcanvas/util/layouts.py +9 -8
- qcanvas/util/paths.py +26 -22
- qcanvas/util/qurl_util.py +1 -1
- qcanvas/util/runtime.py +20 -0
- qcanvas/util/ui_tools.py +121 -7
- 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.0.11.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 -54
- qcanvas/icons/file-download-failed.svg +0 -6
- qcanvas/icons/file-downloaded.svg +0 -6
- qcanvas/icons/file-not-downloaded.svg +0 -6
- qcanvas/icons/file-unknown.svg +0 -6
- qcanvas/icons/main_icon.svg +0 -325
- qcanvas/icons/sync.svg +0 -7
- qcanvas/run.py +0 -30
- qcanvas/ui/main_ui/__init__.py +0 -0
- qcanvas/ui/main_ui/course_viewer_container.py +0 -52
- qcanvas/util/settings/__init__.py +0 -9
- qcanvas/util/themes.py +0 -24
- qcanvas-1.0.11.dist-info/METADATA +0 -61
- qcanvas-1.0.11.dist-info/RECORD +0 -68
- qcanvas-1.0.11.dist-info/entry_points.txt +0 -3
- /qcanvas/ui/course_viewer/tabs/{util.py → constants.py} +0 -0
- /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, QIcon, 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
|
|
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,20 +43,15 @@ _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")
|
|
40
|
-
self.setWindowIcon(
|
|
50
|
+
self.setWindowIcon(icons.branding.main_icon)
|
|
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:
|
|
@@ -74,6 +81,7 @@ class QCanvasWindow(QMainWindow):
|
|
|
74
81
|
shortcut=QKeySequence("Ctrl+S"),
|
|
75
82
|
triggered=self._synchronise_requested,
|
|
76
83
|
parent=app_menu,
|
|
84
|
+
icon=icons.actions.sync,
|
|
77
85
|
)
|
|
78
86
|
|
|
79
87
|
create_qaction(
|
|
@@ -81,17 +89,22 @@ class QCanvasWindow(QMainWindow):
|
|
|
81
89
|
shortcut=QKeySequence("Ctrl+D"),
|
|
82
90
|
triggered=self._open_downloads_folder,
|
|
83
91
|
parent=app_menu,
|
|
92
|
+
icon=icons.actions.open_downloads,
|
|
84
93
|
)
|
|
85
94
|
|
|
86
95
|
create_qaction(
|
|
87
|
-
name="
|
|
96
|
+
name="Open Canvas in browser",
|
|
88
97
|
shortcut=QKeySequence("Ctrl+O"),
|
|
89
98
|
triggered=self._open_quick_auth_in_browser,
|
|
90
99
|
parent=app_menu,
|
|
100
|
+
icon=icons.actions.quick_login,
|
|
91
101
|
)
|
|
92
102
|
|
|
93
103
|
create_qaction(
|
|
94
|
-
name="Mark all as seen",
|
|
104
|
+
name="Mark all as seen",
|
|
105
|
+
triggered=self._clear_new_items,
|
|
106
|
+
parent=app_menu,
|
|
107
|
+
icon=icons.actions.mark_all_read,
|
|
95
108
|
)
|
|
96
109
|
|
|
97
110
|
create_qaction(
|
|
@@ -99,10 +112,11 @@ class QCanvasWindow(QMainWindow):
|
|
|
99
112
|
shortcut=QKeySequence("Ctrl+Q"),
|
|
100
113
|
triggered=lambda: self.close(),
|
|
101
114
|
parent=app_menu,
|
|
115
|
+
icon=icons.actions.exit,
|
|
102
116
|
)
|
|
103
117
|
|
|
104
118
|
options_menu = menu_bar.addMenu("Options")
|
|
105
|
-
|
|
119
|
+
options_menu.setToolTipsVisible(True)
|
|
106
120
|
options_menu.addAction(QuickSyncOption(options_menu))
|
|
107
121
|
options_menu.addAction(SyncOnStartOption(options_menu))
|
|
108
122
|
options_menu.addMenu(AutoDownloadResourcesMenu(options_menu))
|
|
@@ -140,7 +154,6 @@ class QCanvasWindow(QMainWindow):
|
|
|
140
154
|
|
|
141
155
|
@asyncSlot()
|
|
142
156
|
async def _on_app_loaded(self) -> None:
|
|
143
|
-
await self._qcanvas.init()
|
|
144
157
|
self._course_tree.reload(await self._get_terms(), sync_receipt=empty_receipt())
|
|
145
158
|
|
|
146
159
|
if settings.client.sync_on_start:
|
|
@@ -164,12 +177,18 @@ class QCanvasWindow(QMainWindow):
|
|
|
164
177
|
except Exception as e:
|
|
165
178
|
_logger.warning("Sync failed", exc_info=e)
|
|
166
179
|
error = QErrorMessage(self)
|
|
167
|
-
msg = str(e)
|
|
168
180
|
|
|
169
|
-
if isinstance(e,
|
|
170
|
-
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)
|
|
171
187
|
|
|
172
|
-
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
|
+
)
|
|
173
192
|
finally:
|
|
174
193
|
self._operation_semaphore.release()
|
|
175
194
|
self._sync_button.setText("Synchronise")
|
|
@@ -192,14 +211,14 @@ class QCanvasWindow(QMainWindow):
|
|
|
192
211
|
await self._get_courses(), sync_receipt=receipt
|
|
193
212
|
)
|
|
194
213
|
|
|
195
|
-
async def _get_resources(self) ->
|
|
196
|
-
return (await self._qcanvas.
|
|
214
|
+
async def _get_resources(self) -> dict[str, db.Resource]:
|
|
215
|
+
return (await self._qcanvas.load()).resources
|
|
197
216
|
|
|
198
217
|
async def _get_terms(self) -> Sequence[db.Term]:
|
|
199
|
-
return (await self._qcanvas.
|
|
218
|
+
return (await self._qcanvas.load()).terms
|
|
200
219
|
|
|
201
220
|
async def _get_courses(self) -> Sequence[db.Course]:
|
|
202
|
-
return (await self._qcanvas.
|
|
221
|
+
return (await self._qcanvas.load()).courses
|
|
203
222
|
|
|
204
223
|
@Slot(db.Course)
|
|
205
224
|
def _on_course_selected(self, course: Optional[db.Course]) -> None:
|
|
@@ -211,28 +230,38 @@ class QCanvasWindow(QMainWindow):
|
|
|
211
230
|
@asyncSlot(db.Course, str)
|
|
212
231
|
async def _on_course_renamed(self, course: db.Course, new_name: str) -> None:
|
|
213
232
|
_logger.debug("Rename %s -> %s", course.name, new_name)
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
course.configuration.nickname = new_name
|
|
233
|
+
config = course_configs[course.id]
|
|
234
|
+
config.nickname = new_name
|
|
235
|
+
await config.save()
|
|
218
236
|
|
|
219
237
|
@asyncSlot()
|
|
220
238
|
async def _open_quick_auth_in_browser(self) -> None:
|
|
221
239
|
opening_progress_dialog = QProgressDialog("Opening canvas", None, 0, 0, self)
|
|
222
240
|
opening_progress_dialog.setWindowTitle("Please wait")
|
|
223
241
|
opening_progress_dialog.show()
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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()
|
|
228
251
|
|
|
229
252
|
@Slot()
|
|
230
253
|
def _open_downloads_folder(self) -> None:
|
|
231
254
|
directory = self._qcanvas.resource_manager.downloads_folder
|
|
255
|
+
|
|
256
|
+
if self._course_viewer_container.selected_course is not None:
|
|
257
|
+
directory /= self._qcanvas.resource_manager.course_folder_name(
|
|
258
|
+
self._course_viewer_container.selected_course
|
|
259
|
+
)
|
|
260
|
+
|
|
232
261
|
directory.mkdir(parents=True, exist_ok=True)
|
|
233
262
|
|
|
234
263
|
QDesktopServices.openUrl(file_url(directory))
|
|
235
264
|
|
|
236
265
|
@asyncSlot()
|
|
237
266
|
async def _clear_new_items(self) -> None:
|
|
238
|
-
await self._reload(
|
|
267
|
+
await self._reload(empty_receipt())
|
|
@@ -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
|
|
|
@@ -77,35 +76,44 @@ class StatusBarProgressDisplay(QStatusBar):
|
|
|
77
76
|
async with self._lock:
|
|
78
77
|
if self._has_no_tasks:
|
|
79
78
|
self._show_done()
|
|
80
|
-
elif self._has_single_task:
|
|
81
|
-
self._show_single_task_progress(list(self._tasks.items())[0])
|
|
82
79
|
else:
|
|
83
|
-
|
|
80
|
+
tasks = list(self._tasks.items())
|
|
81
|
+
|
|
82
|
+
if self._has_single_task:
|
|
83
|
+
self._show_single_task_progress(tasks[0])
|
|
84
|
+
else:
|
|
85
|
+
self._show_multiple_tasks_progress(tasks)
|
|
84
86
|
|
|
85
87
|
def _show_done(self) -> None:
|
|
86
88
|
_logger.info("Finished tasks. Tasks: %s", self._tasks)
|
|
87
89
|
self.showMessage("Done", 5000)
|
|
88
90
|
self._progress_bar.hide()
|
|
89
91
|
|
|
90
|
-
def _show_single_task_progress(self, task:
|
|
92
|
+
def _show_single_task_progress(self, task: tuple[TaskID, _TaskProgress]) -> None:
|
|
91
93
|
_logger.debug("Single task %s", task)
|
|
92
94
|
id, progress = task
|
|
93
95
|
|
|
94
96
|
self._show_progress(progress)
|
|
95
97
|
self.showMessage(id.step_name)
|
|
96
98
|
|
|
97
|
-
def _show_multiple_tasks_progress(
|
|
99
|
+
def _show_multiple_tasks_progress(
|
|
100
|
+
self, tasks: list[tuple[TaskID, _TaskProgress]]
|
|
101
|
+
) -> None:
|
|
98
102
|
_logger.debug("Multiple tasks %s", tasks)
|
|
99
|
-
self.showMessage(
|
|
103
|
+
self.showMessage(
|
|
104
|
+
f"{len(tasks)} tasks in progress - {', '.join([task[0].step_name for task in tasks])}"
|
|
105
|
+
)
|
|
100
106
|
self._show_progress(self._calculate_progress(tasks))
|
|
101
107
|
|
|
102
|
-
def _calculate_progress(
|
|
103
|
-
|
|
108
|
+
def _calculate_progress(
|
|
109
|
+
self, tasks: list[tuple[TaskID, _TaskProgress]]
|
|
110
|
+
) -> _TaskProgress:
|
|
111
|
+
# Task progresses are floats from 0 to 1, multiplier is used to turn them into ints
|
|
104
112
|
multiplier = 1000
|
|
105
113
|
current_sum = 0
|
|
106
114
|
total_sum = 0
|
|
107
115
|
|
|
108
|
-
for task in tasks:
|
|
116
|
+
for _, task in tasks:
|
|
109
117
|
if task.total != 0:
|
|
110
118
|
current_sum += (task.current / task.total) * multiplier
|
|
111
119
|
|
|
@@ -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
|
+
}
|