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
@@ -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
- import qcanvas_backend.database.types as db
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 qcanvas.ui.main_ui.course_viewer_container import CourseViewerContainer
19
- from qcanvas.ui.main_ui.options.auto_download_resources_option import (
27
+ from .course_viewer_container import CourseViewerContainer
28
+ from .options.auto_download_resources_option import (
20
29
  AutoDownloadResourcesMenu,
21
30
  )
22
- from qcanvas.ui.main_ui.options.quick_sync_option import QuickSyncOption
23
- from qcanvas.ui.main_ui.options.sync_on_start_option import SyncOnStartOption
24
- from qcanvas.ui.main_ui.options.theme_selection_menu import ThemeSelectionMenu
25
- from qcanvas.ui.main_ui.status_bar_progress_display import StatusBarProgressDisplay
26
- from qcanvas.util import auto_downloader, paths, settings
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 = QCanvas[FrontendResourceManager](
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(self._on_app_loaded)
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, httpx.ConnectError):
177
- msg = "You may not be connected to the internet\n - " + 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.showMessage(msg)
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) -> Dict[str, db.Resource]:
203
- return (await self._qcanvas.get_data()).resources
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.get_data()).terms
218
+ return (await self._qcanvas.load()).terms
207
219
 
208
220
  async def _get_courses(self) -> Sequence[db.Course]:
209
- return (await self._qcanvas.get_data()).courses
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
- async with self._qcanvas.database.session() as session:
223
- session.add(course)
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
- QDesktopServices.openUrl(
232
- QUrl(await self._qcanvas.canvas_client.get_temporary_session_url())
233
- )
234
- opening_progress_dialog.close()
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: Tuple[TaskID, _TaskProgress]) -> None:
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[Tuple[TaskID, _TaskProgress]]
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[Tuple[TaskID, _TaskProgress]]
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,4 @@
1
+ from .qml import rc_qml # Do not remove!! Loads the qml resources
2
+ from .comments_pane import CommentsPane
3
+ from .attachments_pane import AttachmentsPane
4
+ from .qml_bridge_types import Attachment, Comment
@@ -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,11 @@
1
+ import QtQuick
2
+
3
+ ThemedRectangle {
4
+ anchors.fill: parent
5
+
6
+ AttachmentsList {
7
+ anchors.fill: parent
8
+ model: submission_files
9
+ palette: theme
10
+ }
11
+ }
@@ -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
+ }