qcanvas 1.2.2__py3-none-any.whl → 2.0.0__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.

Potentially problematic release.


This version of qcanvas might be problematic. Click here for more details.

Files changed (130) hide show
  1. qcanvas/__init__.py +54 -0
  2. qcanvas/app.py +93 -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 +47 -43
  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 +4 -4
  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 +35 -43
  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 +15 -21
  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 +38 -33
  42. qcanvas/ui/{main_ui → qcanvas_window}/status_bar_progress_display.py +5 -6
  43. qcanvas/ui/qml_components/AttachmentsList.ui.qml +15 -0
  44. qcanvas/ui/qml_components/AttachmentsListDelegate.qml +77 -0
  45. qcanvas/ui/qml_components/AttachmentsListModel.qml +19 -0
  46. qcanvas/ui/qml_components/AttachmentsPane.qml +12 -0
  47. qcanvas/ui/qml_components/CommentsList.ui.qml +15 -0
  48. qcanvas/ui/qml_components/CommentsListDelegate.ui.qml +118 -0
  49. qcanvas/ui/qml_components/CommentsListModel.qml +56 -0
  50. qcanvas/ui/qml_components/CommentsPane.qml +12 -0
  51. qcanvas/ui/qml_components/DarkTheme.qml +12 -0
  52. qcanvas/ui/qml_components/DecoratedText.ui.qml +44 -0
  53. qcanvas/ui/qml_components/LightTheme.qml +11 -0
  54. qcanvas/ui/qml_components/Spacer.ui.qml +7 -0
  55. qcanvas/ui/qml_components/ThemedRectangle.qml +37 -0
  56. qcanvas/ui/qml_components/__init__.py +3 -0
  57. qcanvas/ui/qml_components/attachments_pane.py +72 -0
  58. qcanvas/ui/qml_components/comments_pane.py +85 -0
  59. qcanvas/ui/qml_components/qml_bridge_types.py +95 -0
  60. qcanvas/ui/qml_components/qml_pane.py +22 -0
  61. qcanvas/ui/setup/setup_checker.py +1 -1
  62. qcanvas/ui/setup/setup_dialog.py +27 -10
  63. qcanvas/util/auto_downloader.py +9 -7
  64. qcanvas/util/basic_fonts.py +2 -2
  65. qcanvas/util/context_dict.py +12 -0
  66. qcanvas/util/file_icons.py +11 -19
  67. qcanvas/util/layouts.py +5 -7
  68. qcanvas/util/paths.py +17 -6
  69. qcanvas/util/qurl_util.py +1 -1
  70. qcanvas/util/ui_tools.py +118 -8
  71. qcanvas/util/url_checker.py +1 -1
  72. {qcanvas-1.2.2.dist-info → qcanvas-2.0.0.dist-info}/METADATA +12 -10
  73. qcanvas-2.0.0.dist-info/RECORD +91 -0
  74. {qcanvas-1.2.2.dist-info → qcanvas-2.0.0.dist-info}/WHEEL +1 -1
  75. qcanvas-2.0.0.dist-info/entry_points.txt +3 -0
  76. qcanvas/app_start/__init__.py +0 -59
  77. qcanvas/icons/_update_icons.py +0 -89
  78. qcanvas/icons/dark/actions/exit.svg +0 -3
  79. qcanvas/icons/dark/actions/mark_all_read.svg +0 -3
  80. qcanvas/icons/dark/actions/open_downloads.svg +0 -3
  81. qcanvas/icons/dark/actions/quick_login.svg +0 -3
  82. qcanvas/icons/dark/actions/sync.svg +0 -3
  83. qcanvas/icons/dark/branding/logo_transparent.svg +0 -303
  84. qcanvas/icons/dark/options/auto_download.svg +0 -3
  85. qcanvas/icons/dark/options/theme.svg +0 -3
  86. qcanvas/icons/dark/tabs/assignments.svg +0 -3
  87. qcanvas/icons/dark/tabs/mail.svg +0 -3
  88. qcanvas/icons/dark/tabs/pages.svg +0 -3
  89. qcanvas/icons/dark/tree_items/assignment.svg +0 -3
  90. qcanvas/icons/dark/tree_items/mail.svg +0 -3
  91. qcanvas/icons/dark/tree_items/module.svg +0 -3
  92. qcanvas/icons/dark/tree_items/page.svg +0 -3
  93. qcanvas/icons/light/actions/exit.svg +0 -3
  94. qcanvas/icons/light/actions/mark_all_read.svg +0 -3
  95. qcanvas/icons/light/actions/open_downloads.svg +0 -3
  96. qcanvas/icons/light/actions/quick_login.svg +0 -3
  97. qcanvas/icons/light/actions/sync.svg +0 -3
  98. qcanvas/icons/light/branding/logo_transparent.svg +0 -304
  99. qcanvas/icons/light/options/auto_download.svg +0 -3
  100. qcanvas/icons/light/options/ignore_old.svg +0 -3
  101. qcanvas/icons/light/options/include_videos.svg +0 -3
  102. qcanvas/icons/light/options/theme.svg +0 -3
  103. qcanvas/icons/light/tabs/assignments.svg +0 -3
  104. qcanvas/icons/light/tabs/mail.svg +0 -3
  105. qcanvas/icons/light/tabs/pages.svg +0 -3
  106. qcanvas/icons/light/tree_items/assignment.svg +0 -3
  107. qcanvas/icons/light/tree_items/mail.svg +0 -3
  108. qcanvas/icons/light/tree_items/module.svg +0 -3
  109. qcanvas/icons/light/tree_items/page.svg +0 -3
  110. qcanvas/icons/universal/branding/main_icon.svg +0 -325
  111. qcanvas/icons/universal/downloads/download_failed.svg +0 -23
  112. qcanvas/icons/universal/downloads/downloaded.svg +0 -23
  113. qcanvas/icons/universal/downloads/not_downloaded.svg +0 -23
  114. qcanvas/icons/universal/downloads/unknown.svg +0 -6
  115. qcanvas/icons/universal/tabs/assignments_new_content.svg +0 -3
  116. qcanvas/icons/universal/tabs/mail_new_content.svg +0 -3
  117. qcanvas/icons/universal/tabs/pages_new_content.svg +0 -3
  118. qcanvas/icons/universal/tree_items/semester.svg +0 -108
  119. qcanvas/run.py +0 -54
  120. qcanvas/ui/course_viewer/tabs/util.py +0 -11
  121. qcanvas/ui/main_ui/__init__.py +0 -0
  122. qcanvas/util/settings/__init__.py +0 -9
  123. qcanvas/util/themes/__init__.py +0 -2
  124. qcanvas/util/themes/_colour_scheme_helper.py +0 -38
  125. qcanvas/util/themes/_selected_theme.py +0 -10
  126. qcanvas/util/themes/_theme_changed_event.py +0 -17
  127. qcanvas/util/themes/_theme_changer.py +0 -86
  128. qcanvas-1.2.2.dist-info/RECORD +0 -118
  129. qcanvas-1.2.2.dist-info/entry_points.txt +0 -3
  130. /qcanvas/ui/{main_ui → qcanvas_window}/options/__init__.py +0 -0
@@ -1,31 +1,33 @@
1
1
  import logging
2
- from typing import *
3
2
 
4
- import qcanvas_backend.database.types as db
5
- from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
6
- from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
7
- from qtpy.QtCore import Slot
8
- from qtpy.QtWidgets import *
3
+ from libqcanvas import db
4
+ from libqcanvas.net.resources.download.resource_manager import ResourceManager
5
+ from libqcanvas.net.sync.sync_receipt import SyncReceipt
6
+ from PySide6.QtCore import Slot
7
+ from PySide6.QtWidgets import (
8
+ QGridLayout,
9
+ QLabel,
10
+ QWidget,
11
+ QLayout,
12
+ )
9
13
 
10
14
  from qcanvas.ui.course_viewer.content_tree import ContentTree
11
15
  from qcanvas.ui.course_viewer.tabs.resource_rich_browser import ResourceRichBrowser
12
- from qcanvas.util.basic_fonts import bold_font
13
- from qcanvas.util.ui_tools import make_truncatable
14
16
 
15
- _logger = logging.getLogger(__name__)
17
+ import qcanvas.util.ui_tools as ui
16
18
 
17
- T = TypeVar("T", bound=Type["ContentTab"])
19
+ _logger = logging.getLogger(__name__)
18
20
 
19
21
 
20
22
  class ContentTab(QWidget):
21
23
  @classmethod
22
- def create_from_receipt(
24
+ def create_from_receipt[T: ContentTab](
23
25
  cls: T,
24
26
  *,
25
27
  course: db.Course,
26
28
  sync_receipt: SyncReceipt,
27
29
  downloader: ResourceManager,
28
- ) -> Type[T]:
30
+ ) -> type[T]:
29
31
  return cls(course=course, sync_receipt=sync_receipt, downloader=downloader)
30
32
 
31
33
  def __init__(
@@ -36,49 +38,41 @@ class ContentTab(QWidget):
36
38
  downloader: ResourceManager,
37
39
  ):
38
40
  super().__init__()
39
- self._content_vbox = QVBoxLayout()
41
+ self.content_grid = QGridLayout()
40
42
  self._placeholder_text = title_placeholder_text
41
43
  self._title_label = self._create_title_label()
42
- self._info_grid: Optional[QWidget] = None
44
+ self._use_info_grid = False
45
+ self._info_grid = QWidget()
46
+ self._info_grid.hide()
43
47
  self._viewer = ResourceRichBrowser(downloader=downloader)
44
48
  self._explorer = explorer
45
49
 
50
+ self.setLayout(self.content_grid)
46
51
  self._setup_layout()
47
52
  self._explorer.item_selected.connect(self._item_selected)
48
53
 
49
54
  def enable_info_grid(self) -> None:
50
- # Info grid needs to be a widget, so it can be hidden/shown
51
- grid_layout = self.setup_info_grid()
52
-
53
- grid_widget = QWidget()
54
- grid_widget.setLayout(grid_layout)
55
- grid_widget.hide()
56
-
57
- self._info_grid = grid_widget
58
- self._content_vbox.insertWidget(1, grid_widget)
55
+ self._info_grid.setLayout(self.setup_info_grid())
56
+ self._use_info_grid = True
59
57
 
60
58
  def _create_title_label(self) -> QLabel:
61
- title_label = QLabel(self._placeholder_text)
62
- title_label.setFont(bold_font)
63
- make_truncatable(title_label)
64
- return title_label
59
+ return ui.label(
60
+ self._placeholder_text,
61
+ font=ui.font(point_size=12, bold=True),
62
+ allow_truncation=True,
63
+ )
65
64
 
66
- def setup_info_grid(self) -> QGridLayout:
65
+ def setup_info_grid(self) -> QLayout:
67
66
  """
68
67
  Override this if you need an info grid
69
68
  """
70
69
  raise NotImplementedError()
71
70
 
72
71
  def _setup_layout(self) -> None:
73
- parent_layout = QHBoxLayout()
74
- parent_layout.addWidget(self._explorer)
75
-
76
- self._content_vbox.addWidget(self._title_label)
77
- self._content_vbox.addWidget(self._viewer)
78
-
79
- parent_layout.addLayout(self._content_vbox)
80
-
81
- self.setLayout(parent_layout)
72
+ self.content_grid.addWidget(self._explorer, 0, 0, 4, 1)
73
+ self.content_grid.addWidget(self._title_label, 0, 1)
74
+ self.content_grid.addWidget(self._info_grid, 1, 1)
75
+ self.content_grid.addWidget(self._viewer, 2, 1)
82
76
 
83
77
  def reload(self, course: db.Course, *, sync_receipt: SyncReceipt) -> None:
84
78
  self._explorer.reload(course, sync_receipt=sync_receipt)
@@ -95,7 +89,7 @@ class ContentTab(QWidget):
95
89
  self._title_label.setText(item.name)
96
90
  self._viewer.show_content(item)
97
91
 
98
- if self._info_grid is not None:
92
+ if self._use_info_grid:
99
93
  self._info_grid.show()
100
94
  self.update_info_grid(item)
101
95
 
@@ -106,5 +100,5 @@ class ContentTab(QWidget):
106
100
  self._title_label.setText(self._placeholder_text)
107
101
  self._viewer.show_blank(completely_blank=True)
108
102
 
109
- if self._info_grid is not None:
103
+ if self._use_info_grid:
110
104
  self._info_grid.hide()
@@ -1,9 +1,9 @@
1
1
  import logging
2
2
 
3
- import qcanvas_backend.database.types as db
4
- from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
5
- from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
6
- from qtpy.QtWidgets import *
3
+ from libqcanvas import db
4
+ from libqcanvas.net.resources.download.resource_manager import ResourceManager
5
+ from libqcanvas.net.sync.sync_receipt import SyncReceipt
6
+ from PySide6.QtWidgets import QHBoxLayout, QWidget
7
7
 
8
8
  from qcanvas.ui.course_viewer.tabs.file_tab.pages_file_tree import PagesFileTree
9
9
  from qcanvas.util.layouts import layout
@@ -1,11 +1,11 @@
1
1
  import logging
2
- from typing import *
2
+ from abc import ABC
3
3
 
4
- import qcanvas_backend.database.types as db
5
- from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
6
- from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
7
- from qtpy.QtCore import QPoint, Qt, Slot
8
- from qtpy.QtWidgets import *
4
+ from libqcanvas import db
5
+ from libqcanvas.net.resources.download.resource_manager import ResourceManager
6
+ from libqcanvas.net.sync.sync_receipt import SyncReceipt
7
+ from PySide6.QtCore import QPoint, Qt, Slot
8
+ from PySide6.QtWidgets import QHeaderView, QMenu, QTreeWidgetItem
9
9
 
10
10
  from qcanvas.ui.course_viewer.content_tree import ContentTree
11
11
  from qcanvas.ui.course_viewer.tree_widget_data_item import (
@@ -19,10 +19,7 @@ from qcanvas.util.ui_tools import create_qaction
19
19
  _logger = logging.getLogger(__name__)
20
20
 
21
21
 
22
- T = TypeVar("T")
23
-
24
-
25
- class FileTree(ContentTree[db.Course]):
22
+ class FileTree(ContentTree[db.Course], ABC):
26
23
  @classmethod
27
24
  def create_from_receipt(
28
25
  cls,
@@ -1,9 +1,9 @@
1
1
  import logging
2
- from typing import *
2
+ from typing import Sequence
3
3
 
4
- import qcanvas_backend.database.types as db
5
- from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
6
- from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
4
+ from libqcanvas import db
5
+ from libqcanvas.net.resources.download.resource_manager import ResourceManager
6
+ from libqcanvas.net.sync.sync_receipt import SyncReceipt
7
7
 
8
8
  from qcanvas.ui.course_viewer.tabs.file_tab.file_tree import FileTree
9
9
  from qcanvas.ui.memory_tree import MemoryTreeWidgetItem
@@ -12,7 +12,6 @@ _logger = logging.getLogger(__name__)
12
12
 
13
13
 
14
14
  class PagesFileTree(FileTree):
15
-
16
15
  def __init__(self, tree_name: str, *, resource_manager: ResourceManager):
17
16
  super().__init__(
18
17
  tree_name=f"{tree_name}.pages", resource_manager=resource_manager
@@ -24,14 +23,14 @@ class PagesFileTree(FileTree):
24
23
  widgets = []
25
24
 
26
25
  for group in data.modules: # type: db.Module
27
- if len(group.content_items) == 0:
26
+ if not group.pages:
28
27
  continue
29
28
 
30
29
  # Init group_widget lazily to prevent creating pointless tree widgets
31
30
  group_widget: MemoryTreeWidgetItem | None = None
32
31
  items_in_group = set()
33
32
 
34
- for item in group.content_items:
33
+ for item in group.pages:
35
34
  resource_widgets = []
36
35
 
37
36
  for resource in item.resources: # type: db.Resource
@@ -1,16 +1,18 @@
1
1
  import logging
2
- from typing import *
2
+ from typing import override
3
3
 
4
- import qcanvas_backend.database.types as db
5
- from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
6
- from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
7
- from qtpy.QtWidgets import *
4
+ from PySide6.QtCore import Qt
5
+ from libqcanvas import db
6
+ from libqcanvas.net.sync.sync_receipt import SyncReceipt
7
+ from libqcanvas.util import as_local
8
+ from PySide6.QtWidgets import QLabel, QLayout, QMainWindow, QDockWidget
8
9
 
10
+ from qcanvas.backend_connectors import FrontendResourceManager
9
11
  from qcanvas.ui.course_viewer.tabs.content_tab import ContentTab
10
12
  from qcanvas.ui.course_viewer.tabs.mail_tab.mail_tree import MailTree
11
- from qcanvas.ui.course_viewer.tabs.util import date_strftime_format
12
- from qcanvas.util.basic_fonts import bold_label
13
- from qcanvas.util.layouts import grid_layout
13
+ from qcanvas.ui.course_viewer.tabs.constants import date_strftime_format
14
+ import qcanvas.util.ui_tools as ui
15
+ from qcanvas.ui.qml_components import AttachmentsPane
14
16
 
15
17
  _logger = logging.getLogger(__name__)
16
18
 
@@ -22,38 +24,59 @@ class MailTab(ContentTab):
22
24
  *,
23
25
  course: db.Course,
24
26
  sync_receipt: SyncReceipt,
25
- downloader: ResourceManager,
27
+ downloader: FrontendResourceManager,
26
28
  ):
29
+ self._main_container = QMainWindow()
30
+
27
31
  super().__init__(
28
32
  explorer=MailTree.create_from_receipt(course, sync_receipt=sync_receipt),
29
33
  title_placeholder_text="No mail selected",
30
34
  downloader=downloader,
31
35
  )
32
36
 
37
+ self._main_container.setCentralWidget(self._viewer)
38
+ self._files_pane = AttachmentsPane(downloader)
39
+ self._files_dock = ui.dock_widget(
40
+ widget=self._files_pane,
41
+ title="Attachments",
42
+ name="attachments",
43
+ min_size=ui.size(150, 100),
44
+ features=QDockWidget.DockWidgetFeature.DockWidgetMovable,
45
+ )
46
+ self._main_container.addDockWidget(
47
+ Qt.DockWidgetArea.TopDockWidgetArea, self._files_dock
48
+ )
33
49
  self._date_sent_label = QLabel("")
34
50
  self._sender_label = QLabel("")
35
51
 
36
52
  self.enable_info_grid()
37
53
 
38
- def setup_info_grid(self) -> QGridLayout:
39
- grid = grid_layout(
40
- [
41
- [
42
- bold_label("From:"),
43
- self._sender_label,
44
- ],
45
- [
46
- bold_label("Date:"),
47
- self._date_sent_label,
48
- ],
49
- ]
54
+ @override
55
+ def _setup_layout(self) -> None:
56
+ super()._setup_layout()
57
+ self.content_grid.replaceWidget(
58
+ self._viewer,
59
+ self._main_container,
50
60
  )
51
61
 
52
- grid.setColumnStretch(0, 0)
53
- grid.setColumnStretch(1, 1)
54
-
55
- return grid
62
+ def setup_info_grid(self) -> QLayout:
63
+ return ui.form_layout(
64
+ {"From": self._sender_label, "Date": self._date_sent_label},
65
+ )
56
66
 
57
- def update_info_grid(self, mail: db.CourseMessage) -> None:
58
- self._date_sent_label.setText(mail.creation_date.strftime(date_strftime_format))
67
+ def update_info_grid(self, mail: db.Message) -> None:
68
+ self._date_sent_label.setText(
69
+ as_local(mail.creation_date).strftime(date_strftime_format)
70
+ )
59
71
  self._sender_label.setText(mail.sender_name)
72
+
73
+ if mail.attachments:
74
+ self._files_pane.load_files(mail.attachments)
75
+ self._files_dock.show()
76
+ else:
77
+ self._files_dock.hide()
78
+
79
+ @override
80
+ def _show_blank(self) -> None:
81
+ super()._show_blank()
82
+ self._files_dock.hide()
@@ -1,9 +1,9 @@
1
1
  import logging
2
- from typing import *
2
+ from typing import Sequence
3
3
 
4
- import qcanvas_backend.database.types as db
5
- from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
6
- from qtpy.QtWidgets import *
4
+ from libqcanvas import db
5
+ from libqcanvas.net.sync.sync_receipt import SyncReceipt
6
+ from PySide6.QtWidgets import QHeaderView
7
7
 
8
8
  from qcanvas import icons
9
9
  from qcanvas.ui.course_viewer.content_tree import ContentTree
@@ -13,11 +13,10 @@ _logger = logging.getLogger(__name__)
13
13
 
14
14
 
15
15
  class MailTree(ContentTree[db.Course]):
16
-
17
16
  def __init__(self, course_id: str):
18
17
  super().__init__(
19
18
  tree_name=f"course.{course_id}.mail",
20
- emit_selection_signal_for_type=db.CourseMessage,
19
+ emit_selection_signal_for_type=db.Message,
21
20
  )
22
21
 
23
22
  self.ui_setup(
@@ -37,7 +36,7 @@ class MailTree(ContentTree[db.Course]):
37
36
  ) -> Sequence[TreeWidgetDataItem]:
38
37
  widgets = []
39
38
 
40
- for message in course.messages: # type: db.CourseMessage
39
+ for message in course.messages: # type: db.Message
41
40
  message_widget = self._create_mail_widget(message, sync_receipt)
42
41
  message_widget.setIcon(0, icons.tree_items.mail)
43
42
  widgets.append(message_widget)
@@ -45,7 +44,7 @@ class MailTree(ContentTree[db.Course]):
45
44
  return widgets
46
45
 
47
46
  def _create_mail_widget(
48
- self, message: db.CourseMessage, sync_receipt: SyncReceipt
47
+ self, message: db.Message, sync_receipt: SyncReceipt
49
48
  ) -> TreeWidgetDataItem:
50
49
  message_widget = TreeWidgetDataItem(
51
50
  id=message.id,
@@ -1,8 +1,8 @@
1
1
  import logging
2
2
 
3
- import qcanvas_backend.database.types as db
4
- from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
5
- from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
3
+ from libqcanvas import db
4
+ from libqcanvas.net.resources.download.resource_manager import ResourceManager
5
+ from libqcanvas.net.sync.sync_receipt import SyncReceipt
6
6
 
7
7
  from qcanvas.ui.course_viewer.tabs.content_tab import ContentTab
8
8
  from qcanvas.ui.course_viewer.tabs.page_tab.page_tree import PageTree
@@ -1,9 +1,9 @@
1
1
  import logging
2
2
  from typing import Sequence
3
3
 
4
- import qcanvas_backend.database.types as db
5
- from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
6
- from qtpy.QtCore import Qt
4
+ from libqcanvas import db
5
+ from libqcanvas.net.sync.sync_receipt import SyncReceipt
6
+ from PySide6.QtCore import Qt
7
7
 
8
8
  from qcanvas import icons
9
9
  from qcanvas.ui.course_viewer.content_tree import ContentTree
@@ -17,7 +17,7 @@ class PageTree(ContentTree[db.Course]):
17
17
  def __init__(self, course_id: str):
18
18
  super().__init__(
19
19
  tree_name=f"course.{course_id}.modules",
20
- emit_selection_signal_for_type=db.ModulePage,
20
+ emit_selection_signal_for_type=db.Page,
21
21
  )
22
22
 
23
23
  self.ui_setup(
@@ -52,7 +52,7 @@ class PageTree(ContentTree[db.Course]):
52
52
  return module_widget
53
53
 
54
54
  def _create_page_widget(
55
- self, page: db.ModulePage, sync_receipt: SyncReceipt
55
+ self, page: db.Page, sync_receipt: SyncReceipt
56
56
  ) -> TreeWidgetDataItem:
57
57
  page_widget = TreeWidgetDataItem(id=page.id, data=page, strings=[page.name])
58
58
  page_widget.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable)
@@ -2,25 +2,23 @@ import html
2
2
  import logging
3
3
  from typing import Optional
4
4
 
5
- import qcanvas_backend.database.types as db
5
+ from libqcanvas import db
6
6
  from bs4 import BeautifulSoup, Tag
7
+ from libqcanvas.net.resources.extracting.no_extractor_error import NoExtractorError
8
+ from libqcanvas.util import is_link_invisible
9
+ from PySide6.QtCore import QUrl, Slot
10
+ from PySide6.QtGui import QDesktopServices
11
+ from PySide6.QtWidgets import QTextBrowser
7
12
  from qasync import asyncSlot
8
- from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
9
- from qcanvas_backend.net.resources.extracting.no_extractor_error import NoExtractorError
10
- from qcanvas_backend.util import is_link_invisible
11
- from qtpy.QtCore import QUrl, Slot
12
- from qtpy.QtGui import QDesktopServices
13
- from qtpy.QtWidgets import QTextBrowser
14
13
 
15
14
  from qcanvas.backend_connectors import FrontendResourceManager
16
15
  from qcanvas.util.html_cleaner import clean_up_html
17
- from qcanvas.util.qurl_util import file_url
18
16
 
19
17
  _logger = logging.getLogger(__name__)
20
18
 
21
19
 
22
20
  class ResourceRichBrowser(QTextBrowser):
23
- def __init__(self, downloader: ResourceManager):
21
+ def __init__(self, downloader: FrontendResourceManager):
24
22
  super().__init__()
25
23
  self._downloader = downloader
26
24
  self._content: Optional[db.CourseContentItem] = None
@@ -30,9 +28,8 @@ class ResourceRichBrowser(QTextBrowser):
30
28
  self.setOpenLinks(False)
31
29
  self.anchorClicked.connect(self._open_url)
32
30
 
33
- if isinstance(self._downloader, FrontendResourceManager):
34
- self._downloader.download_finished.connect(self._download_updated)
35
- self._downloader.download_failed.connect(self._download_updated)
31
+ self._downloader.download_finished.connect(self._download_updated)
32
+ self._downloader.download_failed.connect(self._download_updated)
36
33
 
37
34
  def show_blank(self, completely_blank: bool = False) -> None:
38
35
  if completely_blank:
@@ -43,14 +40,14 @@ class ResourceRichBrowser(QTextBrowser):
43
40
  self._content = None
44
41
  self._current_content_resources.clear()
45
42
 
46
- def show_content(self, page: db.CourseContentItem) -> None:
43
+ def show_content(self, page: db.AnyContentItem) -> None:
47
44
  if page.body is None:
48
45
  self.show_blank()
49
46
  else:
50
47
  self._collect_resources(page)
51
48
  self._show_page_content(page)
52
49
 
53
- def _collect_resources(self, page: db.CourseContentItem):
50
+ def _collect_resources(self, page: db.AnyContentItem):
54
51
  self._current_content_resources = {
55
52
  resource.id: resource for resource in page.resources
56
53
  }
@@ -137,16 +134,13 @@ class ResourceRichBrowser(QTextBrowser):
137
134
  resource = self._current_content_resources[resource_id]
138
135
 
139
136
  try:
140
- await self._downloader.download(resource)
137
+ await self._downloader.download_and_open(resource)
141
138
  except Exception as e:
142
139
  _logger.warning(
143
140
  "Download of resource id=%s failed", resource_id, exc_info=e
144
141
  )
145
142
  return
146
143
 
147
- resource_path = file_url(self._downloader.resource_download_location(resource))
148
- QDesktopServices.openUrl(resource_path)
149
-
150
144
  @Slot(db.Resource)
151
145
  def _download_updated(self, resource: db.Resource) -> None:
152
146
  if self._content is not None and resource.id in self._current_content_resources:
@@ -163,8 +157,8 @@ class ResourceRichBrowser(QTextBrowser):
163
157
  _logger.warning(
164
158
  "Resource has diverged from current loaded data, applying bandaid fix"
165
159
  )
166
- self._current_content_resources[resource.id].download_state = (
167
- resource.download_state
168
- )
160
+ self._current_content_resources[
161
+ resource.id
162
+ ].download_state = resource.download_state
169
163
 
170
164
  self._show_page_content(self._content)
@@ -1,6 +1,6 @@
1
1
  from typing import List, Optional
2
2
 
3
- from qtpy.QtWidgets import QTreeWidgetItem
3
+ from PySide6.QtWidgets import QTreeWidgetItem
4
4
 
5
5
  from qcanvas.ui.memory_tree import MemoryTreeWidgetItem
6
6
 
@@ -1,49 +1,41 @@
1
+ import asyncio
1
2
  import logging
2
3
  from pathlib import Path
3
- from typing import *
4
4
 
5
- from lightdb import LightDB, Model
5
+ from aiofile import async_open
6
6
 
7
7
  from qcanvas.util import paths
8
8
 
9
9
  _logger = logging.getLogger(__name__)
10
10
 
11
11
 
12
- def _storage_path() -> Path:
13
- path = paths.ui_storage() / "TREE.DB"
14
- path.parent.mkdir(parents=True, exist_ok=True)
15
- return path
16
-
17
-
18
- _state_db = LightDB(str(_storage_path()))
19
-
20
-
21
- class _TreeState(Model, table="trees", db=_state_db):
22
- tree_name: str
23
- collapsed_items: List[str] = []
24
-
12
+ class TreeMemory:
13
+ def __init__(self, tree_name: str):
14
+ self._tree_name = tree_name
15
+ self._loaded = False
16
+ self._collapsed_items: set[str] = set()
25
17
 
26
- def _get_or_create_state(name: str) -> _TreeState:
27
- state = _TreeState.get(tree_name=name)
18
+ def load(self, force: bool = False):
19
+ if force or not self._loaded:
20
+ self._loaded = True
28
21
 
29
- if state is None:
30
- state = _TreeState.create(tree_name=name)
31
- # Initialise the list here! Or else every instance has the same list object
32
- state.collapsed_items = []
33
- # Important or instances will get duplicated data in some cases
34
- state.save()
35
- return state
36
- else:
37
- return state
22
+ if not self._storage_path.exists():
23
+ # Nothing to do
24
+ return
38
25
 
26
+ # fixme this blocks the event loop, but using aiofile will require significant changes to other
27
+ # parts of the code to accommodate these methods now being async.
28
+ # this will really only have any noticeable effect on slow disks, and only ever happens once anyway.
29
+ with open(self._storage_path, "rt") as file:
30
+ lines = file.read().splitlines()
31
+ _logger.debug("Tree % loaded %s", self._tree_name, lines)
32
+ self._collapsed_items.update(lines)
39
33
 
40
- class TreeMemory:
41
- def __init__(self, tree_name: str):
42
- self._tree_name = tree_name
43
- self._state = _get_or_create_state(tree_name)
34
+ async def save(self):
35
+ assert self._loaded, "Memory is not loaded yet"
44
36
 
45
- def is_expanded(self, node_id: str) -> bool:
46
- return node_id in self._state.expanded_items
37
+ async with async_open(self._storage_path, "wt") as file:
38
+ await file.write("\n".join(self._collapsed_items))
47
39
 
48
40
  def expanded(self, node_id: str) -> None:
49
41
  self.set_expanded(node_id, True)
@@ -52,16 +44,27 @@ class TreeMemory:
52
44
  self.set_expanded(node_id, False)
53
45
 
54
46
  def set_expanded(self, node_id: str, expanded: bool) -> None:
55
- contains = node_id in self._state.collapsed_items
47
+ self.load()
56
48
 
57
- # fixme when using a slow usb stick, this can momentarily block the event loop.
58
- if expanded and contains:
59
- self._state.collapsed_items.remove(node_id)
60
- self._state.save()
61
- elif not expanded and not contains:
62
- self._state.collapsed_items.append(node_id)
63
- self._state.save()
49
+ if expanded and node_id in self._collapsed_items:
50
+ self._collapsed_items.remove(node_id)
51
+ else:
52
+ self._collapsed_items.add(node_id)
53
+
54
+ # hack: avoid blocking the event loop
55
+ asyncio.create_task(self.save())
56
+
57
+ @property
58
+ def collapsed_ids(self) -> set[str]:
59
+ assert self._loaded, "Memory not loaded yet"
60
+ return self._collapsed_items
64
61
 
65
62
  @property
66
- def collapsed_ids(self) -> List[str]:
67
- return self._state.collapsed_items
63
+ def _storage_path(self) -> Path:
64
+ path = (
65
+ paths.data_storage()
66
+ / "tree_state"
67
+ / f"{self._tree_name}_collapsed_items.txt"
68
+ )
69
+ path.parent.mkdir(parents=True, exist_ok=True)
70
+ return path