qcanvas 1.0.12.dev3__py3-none-any.whl → 1.2.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.
Files changed (97) hide show
  1. qcanvas/app_start/__init__.py +6 -1
  2. qcanvas/icons/__init__.py +55 -6
  3. qcanvas/icons/_icon_type.py +42 -0
  4. qcanvas/icons/_update_icons.py +89 -0
  5. qcanvas/icons/dark/actions/exit.svg +3 -0
  6. qcanvas/icons/dark/actions/mark_all_read.svg +3 -0
  7. qcanvas/icons/dark/actions/open_downloads.svg +3 -0
  8. qcanvas/icons/dark/actions/quick_login.svg +3 -0
  9. qcanvas/icons/dark/actions/sync.svg +3 -0
  10. qcanvas/icons/dark/branding/logo_transparent.svg +303 -0
  11. qcanvas/icons/dark/options/auto_download.svg +3 -0
  12. qcanvas/icons/dark/options/theme.svg +3 -0
  13. qcanvas/icons/dark/tabs/assignments.svg +3 -0
  14. qcanvas/icons/dark/tabs/mail.svg +3 -0
  15. qcanvas/icons/dark/tabs/pages.svg +3 -0
  16. qcanvas/icons/dark/tree_items/assignment.svg +3 -0
  17. qcanvas/icons/dark/tree_items/mail.svg +3 -0
  18. qcanvas/icons/dark/tree_items/module.svg +3 -0
  19. qcanvas/icons/dark/tree_items/page.svg +3 -0
  20. qcanvas/icons/icons.qrc +44 -8
  21. qcanvas/icons/light/actions/exit.svg +3 -0
  22. qcanvas/icons/light/actions/mark_all_read.svg +3 -0
  23. qcanvas/icons/light/actions/open_downloads.svg +3 -0
  24. qcanvas/icons/light/actions/quick_login.svg +3 -0
  25. qcanvas/icons/light/actions/sync.svg +3 -0
  26. qcanvas/icons/light/branding/logo_transparent.svg +304 -0
  27. qcanvas/icons/light/options/auto_download.svg +3 -0
  28. qcanvas/icons/light/options/ignore_old.svg +3 -0
  29. qcanvas/icons/light/options/include_videos.svg +3 -0
  30. qcanvas/icons/light/options/theme.svg +3 -0
  31. qcanvas/icons/light/tabs/assignments.svg +3 -0
  32. qcanvas/icons/light/tabs/mail.svg +3 -0
  33. qcanvas/icons/light/tabs/pages.svg +3 -0
  34. qcanvas/icons/light/tree_items/assignment.svg +3 -0
  35. qcanvas/icons/light/tree_items/mail.svg +3 -0
  36. qcanvas/icons/light/tree_items/module.svg +3 -0
  37. qcanvas/icons/light/tree_items/page.svg +3 -0
  38. qcanvas/icons/rc_icons.py +2165 -355
  39. qcanvas/icons/universal/downloads/download_failed.svg +23 -0
  40. qcanvas/icons/universal/downloads/downloaded.svg +23 -0
  41. qcanvas/icons/universal/downloads/not_downloaded.svg +23 -0
  42. qcanvas/icons/universal/tabs/assignments_new_content.svg +3 -0
  43. qcanvas/icons/universal/tabs/mail_new_content.svg +3 -0
  44. qcanvas/icons/universal/tabs/pages_new_content.svg +3 -0
  45. qcanvas/icons/universal/tree_items/semester.svg +108 -0
  46. qcanvas/run.py +24 -0
  47. qcanvas/ui/course_viewer/content_tree.py +28 -7
  48. qcanvas/ui/course_viewer/course_tree/__init__.py +1 -0
  49. qcanvas/ui/course_viewer/course_tree/_course_icon_generator.py +86 -0
  50. qcanvas/ui/course_viewer/{course_tree.py → course_tree/course_tree.py} +20 -6
  51. qcanvas/ui/course_viewer/course_viewer.py +71 -24
  52. qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +15 -14
  53. qcanvas/ui/course_viewer/tabs/file_tab/__init__.py +1 -0
  54. qcanvas/ui/course_viewer/tabs/file_tab/file_tab.py +46 -0
  55. qcanvas/ui/course_viewer/tabs/file_tab/file_tree.py +99 -0
  56. qcanvas/ui/course_viewer/tabs/file_tab/pages_file_tree.py +56 -0
  57. qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +11 -11
  58. qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +13 -11
  59. qcanvas/ui/course_viewer/tabs/resource_rich_browser.py +57 -56
  60. qcanvas/ui/course_viewer/tabs/util.py +10 -0
  61. qcanvas/ui/course_viewer/tree_widget_data_item.py +22 -0
  62. qcanvas/ui/main_ui/course_viewer_container.py +46 -3
  63. qcanvas/ui/main_ui/options/auto_download_resources_option.py +3 -1
  64. qcanvas/ui/main_ui/options/theme_selection_menu.py +2 -0
  65. qcanvas/ui/main_ui/qcanvas_window.py +18 -5
  66. qcanvas/ui/main_ui/status_bar_progress_display.py +17 -8
  67. qcanvas/ui/memory_tree/_tree_memory.py +1 -0
  68. qcanvas/ui/memory_tree/memory_tree_widget.py +2 -2
  69. qcanvas/ui/setup/setup_checker.py +2 -2
  70. qcanvas/ui/setup/setup_dialog.py +145 -66
  71. qcanvas/util/__init__.py +0 -2
  72. qcanvas/util/auto_downloader.py +1 -2
  73. qcanvas/util/file_icons.py +54 -0
  74. qcanvas/util/html_cleaner.py +2 -0
  75. qcanvas/util/layouts.py +5 -2
  76. qcanvas/util/paths.py +15 -26
  77. qcanvas/util/runtime.py +20 -0
  78. qcanvas/util/settings/_client_settings.py +11 -2
  79. qcanvas/util/settings/_mapped_setting.py +6 -1
  80. qcanvas/util/themes/__init__.py +2 -0
  81. qcanvas/util/themes/_colour_scheme_helper.py +38 -0
  82. qcanvas/util/themes/_selected_theme.py +10 -0
  83. qcanvas/util/themes/_theme_changed_event.py +17 -0
  84. qcanvas/util/themes/_theme_changer.py +86 -0
  85. qcanvas/util/ui_tools.py +5 -1
  86. {qcanvas-1.0.12.dev3.dist-info → qcanvas-1.2.0.dist-info}/METADATA +16 -6
  87. qcanvas-1.2.0.dist-info/RECORD +118 -0
  88. qcanvas/icons/file-download-failed.svg +0 -6
  89. qcanvas/icons/file-downloaded.svg +0 -6
  90. qcanvas/icons/file-not-downloaded.svg +0 -6
  91. qcanvas/icons/sync.svg +0 -7
  92. qcanvas/util/themes.py +0 -24
  93. qcanvas-1.0.12.dev3.dist-info/RECORD +0 -68
  94. /qcanvas/icons/{main_icon.svg → universal/branding/main_icon.svg} +0 -0
  95. /qcanvas/icons/{file-unknown.svg → universal/downloads/unknown.svg} +0 -0
  96. {qcanvas-1.0.12.dev3.dist-info → qcanvas-1.2.0.dist-info}/WHEEL +0 -0
  97. {qcanvas-1.0.12.dev3.dist-info → qcanvas-1.2.0.dist-info}/entry_points.txt +0 -0
@@ -6,7 +6,9 @@ from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
6
6
  from qtpy.QtCore import Qt
7
7
  from qtpy.QtWidgets import QHeaderView
8
8
 
9
+ from qcanvas import icons
9
10
  from qcanvas.ui.course_viewer.content_tree import ContentTree
11
+ from qcanvas.ui.course_viewer.tree_widget_data_item import TreeWidgetDataItem
10
12
  from qcanvas.ui.memory_tree import MemoryTreeWidgetItem
11
13
 
12
14
  _logger = logging.getLogger(__name__)
@@ -18,6 +20,7 @@ class AssignmentTree(ContentTree[db.Course]):
18
20
  tree_name=f"course.{course_id}.assignment_groups",
19
21
  emit_selection_signal_for_type=db.Assignment,
20
22
  )
23
+
21
24
  self.ui_setup(
22
25
  header_text=["Assignments", "Weight"],
23
26
  indentation=15,
@@ -25,13 +28,9 @@ class AssignmentTree(ContentTree[db.Course]):
25
28
  min_width=150,
26
29
  )
27
30
 
28
- self._adjust_header()
29
-
30
- def _adjust_header(self) -> None:
31
- header = self.header()
32
- header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
33
- header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
34
- header.setStretchLastSection(False)
31
+ self.set_columns_resize_mode(
32
+ [QHeaderView.ResizeMode.Stretch, QHeaderView.ResizeMode.ResizeToContents]
33
+ )
35
34
 
36
35
  def create_tree_items(
37
36
  self, course: db.Course, sync_receipt: SyncReceipt
@@ -39,8 +38,11 @@ class AssignmentTree(ContentTree[db.Course]):
39
38
  widgets = []
40
39
 
41
40
  for assignment_group in course.assignment_groups: # type: db.AssignmentGroup
41
+ if len(assignment_group.assignments) == 0:
42
+ continue
43
+
42
44
  assignment_group_widget = self._create_assignment_group_widget(
43
- assignment_group, sync_receipt
45
+ assignment_group
44
46
  )
45
47
 
46
48
  widgets.append(assignment_group_widget)
@@ -54,7 +56,7 @@ class AssignmentTree(ContentTree[db.Course]):
54
56
  return widgets
55
57
 
56
58
  def _create_assignment_group_widget(
57
- self, assignment_group: db.AssignmentGroup, sync_receipt: SyncReceipt
59
+ self, assignment_group: db.AssignmentGroup
58
60
  ) -> MemoryTreeWidgetItem:
59
61
  assignment_group_widget = MemoryTreeWidgetItem(
60
62
  id=assignment_group.id,
@@ -63,22 +65,21 @@ class AssignmentTree(ContentTree[db.Course]):
63
65
  )
64
66
 
65
67
  assignment_group_widget.setFlags(Qt.ItemFlag.ItemIsEnabled)
66
-
67
- if sync_receipt.was_updated(assignment_group):
68
- self.mark_as_unseen(assignment_group_widget)
68
+ assignment_group_widget.setIcon(0, icons.tree_items.module)
69
69
 
70
70
  return assignment_group_widget
71
71
 
72
72
  def _create_assignment_widget(
73
73
  self, assignment: db.Assignment, sync_receipt: SyncReceipt
74
- ) -> MemoryTreeWidgetItem:
75
- assignment_widget = MemoryTreeWidgetItem(
74
+ ) -> TreeWidgetDataItem:
75
+ assignment_widget = TreeWidgetDataItem(
76
76
  id=assignment.id, data=assignment, strings=[assignment.name]
77
77
  )
78
78
 
79
79
  assignment_widget.setFlags(
80
80
  Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable
81
81
  )
82
+ assignment_widget.setIcon(0, icons.tree_items.assignment)
82
83
 
83
84
  if sync_receipt.was_updated(assignment):
84
85
  self.mark_as_unseen(assignment_widget)
@@ -0,0 +1 @@
1
+ from .file_tab import FileTab
@@ -0,0 +1,46 @@
1
+ import logging
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 *
7
+
8
+ from qcanvas.ui.course_viewer.tabs.file_tab.pages_file_tree import PagesFileTree
9
+ from qcanvas.util.layouts import layout
10
+
11
+ _logger = logging.getLogger(__name__)
12
+
13
+
14
+ class FileTab(QWidget):
15
+ @staticmethod
16
+ def create_from_receipt(
17
+ *,
18
+ course: db.Course,
19
+ sync_receipt: SyncReceipt,
20
+ downloader: ResourceManager,
21
+ ) -> "FileTab":
22
+ return FileTab(course=course, sync_receipt=sync_receipt, downloader=downloader)
23
+
24
+ def __init__(
25
+ self,
26
+ course: db.Course,
27
+ downloader: ResourceManager,
28
+ *,
29
+ sync_receipt: SyncReceipt = None,
30
+ ):
31
+ super().__init__()
32
+
33
+ self._page_file_tree = PagesFileTree.create_from_receipt(
34
+ course, sync_receipt=sync_receipt, resource_manager=downloader
35
+ )
36
+ # self._assignment_file_tree = FileTree.create_from_receipt(
37
+ # course,
38
+ # sync_receipt=sync_receipt,
39
+ # mode="assignments",
40
+ # resource_manager=downloader,
41
+ # )
42
+
43
+ self.setLayout(layout(QHBoxLayout, self._page_file_tree))
44
+
45
+ def reload(self, course: db.Course, *, sync_receipt: SyncReceipt) -> None:
46
+ pass
@@ -0,0 +1,99 @@
1
+ import logging
2
+ from typing import *
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 *
9
+
10
+ from qcanvas.ui.course_viewer.content_tree import ContentTree
11
+ from qcanvas.ui.course_viewer.tree_widget_data_item import (
12
+ AnyTreeDataItem,
13
+ TreeWidgetDataItem,
14
+ )
15
+ from qcanvas.ui.memory_tree import MemoryTreeWidgetItem
16
+ from qcanvas.util.file_icons import icon_for_filename
17
+ from qcanvas.util.ui_tools import create_qaction
18
+
19
+ _logger = logging.getLogger(__name__)
20
+
21
+
22
+ T = TypeVar("T")
23
+
24
+
25
+ class FileTree(ContentTree[db.Course]):
26
+ @classmethod
27
+ def create_from_receipt(
28
+ cls,
29
+ course: db.Course,
30
+ *,
31
+ sync_receipt: SyncReceipt,
32
+ resource_manager: ResourceManager,
33
+ ) -> "FileTree":
34
+ tree = cls(tree_name=course.id, resource_manager=resource_manager)
35
+ tree.reload(course, sync_receipt=sync_receipt)
36
+ return tree
37
+
38
+ def __init__(self, tree_name: str, *, resource_manager: ResourceManager):
39
+ super().__init__(tree_name, emit_selection_signal_for_type=object)
40
+ self._resource_manager = resource_manager
41
+
42
+ self.ui_setup(header_text=["File", "Date"])
43
+ self.set_columns_resize_mode(
44
+ [QHeaderView.ResizeMode.Stretch, QHeaderView.ResizeMode.ResizeToContents]
45
+ )
46
+
47
+ self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
48
+ self.customContextMenuRequested.connect(self._context_menu)
49
+
50
+ def _create_group_widget(
51
+ self, group: db.ContentGroup, sync_receipt: SyncReceipt
52
+ ) -> MemoryTreeWidgetItem:
53
+ group_widget = MemoryTreeWidgetItem(
54
+ id=group.id, data=group, strings=[group.name]
55
+ )
56
+
57
+ group_widget.setFlags(Qt.ItemFlag.ItemIsEnabled)
58
+
59
+ if sync_receipt.was_updated(group):
60
+ self.mark_as_unseen(group_widget)
61
+
62
+ return group_widget
63
+
64
+ def _create_resource_widget(
65
+ self, resource: db.Resource, sync_receipt: SyncReceipt
66
+ ) -> QTreeWidgetItem:
67
+ # fixme the reesource widget items shouls NOT be a memory widget item because they can't be collapsed, but
68
+ # mostly because the same file can appear in the tree multiple times in different places, which memory tree
69
+ # can NOT deal with!
70
+ item_widget = TreeWidgetDataItem(
71
+ id=resource.id,
72
+ data=resource,
73
+ strings=[resource.file_name, str(resource.discovery_date.date())],
74
+ )
75
+ item_widget.setIcon(
76
+ 0,
77
+ icon_for_filename(resource.file_name),
78
+ )
79
+ item_widget.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable)
80
+
81
+ if sync_receipt.was_updated(resource):
82
+ self.mark_as_unseen(item_widget)
83
+
84
+ return item_widget
85
+
86
+ @Slot(QPoint)
87
+ def _context_menu(self, point: QPoint) -> None:
88
+ item = self.itemAt(point)
89
+
90
+ if isinstance(item, AnyTreeDataItem):
91
+ menu = QMenu()
92
+ create_qaction(
93
+ name="Test",
94
+ parent=menu,
95
+ triggered=lambda: print(f"Clicked {item.extra_data.file_name}"),
96
+ )
97
+ menu.addAction("Another thing")
98
+
99
+ menu.exec(self.mapToGlobal(point))
@@ -0,0 +1,56 @@
1
+ import logging
2
+ from typing import *
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
+
8
+ from qcanvas.ui.course_viewer.tabs.file_tab.file_tree import FileTree
9
+ from qcanvas.ui.memory_tree import MemoryTreeWidgetItem
10
+
11
+ _logger = logging.getLogger(__name__)
12
+
13
+
14
+ class PagesFileTree(FileTree):
15
+
16
+ def __init__(self, tree_name: str, *, resource_manager: ResourceManager):
17
+ super().__init__(
18
+ tree_name=f"{tree_name}.pages", resource_manager=resource_manager
19
+ )
20
+
21
+ def create_tree_items(
22
+ self, data: db.Course, sync_receipt: SyncReceipt
23
+ ) -> Sequence[MemoryTreeWidgetItem]:
24
+ widgets = []
25
+
26
+ for group in data.modules: # type: db.Module
27
+ if len(group.content_items) == 0:
28
+ continue
29
+
30
+ # Init group_widget lazily to prevent creating pointless tree widgets
31
+ group_widget: MemoryTreeWidgetItem | None = None
32
+ items_in_group = set()
33
+
34
+ for item in group.content_items:
35
+ resource_widgets = []
36
+
37
+ for resource in item.resources: # type: db.Resource
38
+ if resource.id not in items_in_group:
39
+ items_in_group.add(resource.id)
40
+
41
+ if group_widget is None:
42
+ group_widget = self._create_group_widget(
43
+ group, sync_receipt
44
+ )
45
+
46
+ resource_widgets.append(
47
+ self._create_resource_widget(resource, sync_receipt)
48
+ )
49
+
50
+ if len(resource_widgets) > 0:
51
+ group_widget.addChildren(resource_widgets)
52
+
53
+ if group_widget is not None:
54
+ widgets.append(group_widget)
55
+
56
+ return widgets
@@ -5,8 +5,9 @@ import qcanvas_backend.database.types as db
5
5
  from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
6
6
  from qtpy.QtWidgets import *
7
7
 
8
+ from qcanvas import icons
8
9
  from qcanvas.ui.course_viewer.content_tree import ContentTree
9
- from qcanvas.ui.memory_tree import MemoryTreeWidgetItem
10
+ from qcanvas.ui.course_viewer.tree_widget_data_item import TreeWidgetDataItem
10
11
 
11
12
  _logger = logging.getLogger(__name__)
12
13
 
@@ -18,36 +19,35 @@ class MailTree(ContentTree[db.Course]):
18
19
  tree_name=f"course.{course_id}.mail",
19
20
  emit_selection_signal_for_type=db.CourseMessage,
20
21
  )
22
+
21
23
  self.ui_setup(
22
24
  header_text=["Subject", "Sender"],
23
25
  max_width=500,
24
26
  min_width=300,
25
27
  indentation=20,
28
+ alternating_row_colours=True,
26
29
  )
27
30
 
28
- self._adjust_header()
29
-
30
- def _adjust_header(self) -> None:
31
- header = self.header()
32
- header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
33
- header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
34
- header.setStretchLastSection(False)
31
+ self.set_columns_resize_mode(
32
+ [QHeaderView.ResizeMode.Stretch, QHeaderView.ResizeMode.ResizeToContents]
33
+ )
35
34
 
36
35
  def create_tree_items(
37
36
  self, course: db.Course, sync_receipt: SyncReceipt
38
- ) -> Sequence[MemoryTreeWidgetItem]:
37
+ ) -> Sequence[TreeWidgetDataItem]:
39
38
  widgets = []
40
39
 
41
40
  for message in course.messages: # type: db.CourseMessage
42
41
  message_widget = self._create_mail_widget(message, sync_receipt)
42
+ message_widget.setIcon(0, icons.tree_items.mail)
43
43
  widgets.append(message_widget)
44
44
 
45
45
  return widgets
46
46
 
47
47
  def _create_mail_widget(
48
48
  self, message: db.CourseMessage, sync_receipt: SyncReceipt
49
- ) -> MemoryTreeWidgetItem:
50
- message_widget = MemoryTreeWidgetItem(
49
+ ) -> TreeWidgetDataItem:
50
+ message_widget = TreeWidgetDataItem(
51
51
  id=message.id,
52
52
  data=message,
53
53
  strings=[message.name, message.sender_name],
@@ -1,11 +1,13 @@
1
1
  import logging
2
- from typing import Optional, Sequence
2
+ from typing import Sequence
3
3
 
4
4
  import qcanvas_backend.database.types as db
5
5
  from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
6
6
  from qtpy.QtCore import Qt
7
7
 
8
+ from qcanvas import icons
8
9
  from qcanvas.ui.course_viewer.content_tree import ContentTree
10
+ from qcanvas.ui.course_viewer.tree_widget_data_item import TreeWidgetDataItem
9
11
  from qcanvas.ui.memory_tree import MemoryTreeWidgetItem
10
12
 
11
13
  _logger = logging.getLogger(__name__)
@@ -17,6 +19,7 @@ class PageTree(ContentTree[db.Course]):
17
19
  tree_name=f"course.{course_id}.modules",
18
20
  emit_selection_signal_for_type=db.ModulePage,
19
21
  )
22
+
20
23
  self.ui_setup(
21
24
  header_text="Content", indentation=15, max_width=300, min_width=150
22
25
  )
@@ -27,7 +30,10 @@ class PageTree(ContentTree[db.Course]):
27
30
  widgets = []
28
31
 
29
32
  for module in course.modules: # type: db.Module
30
- module_widget = self._create_module_widget(module, sync_receipt)
33
+ if len(module.pages) == 0:
34
+ continue
35
+
36
+ module_widget = self._create_module_widget(module)
31
37
  widgets.append(module_widget)
32
38
 
33
39
  for page in module.pages: # type: db.ModulePage
@@ -36,25 +42,21 @@ class PageTree(ContentTree[db.Course]):
36
42
 
37
43
  return widgets
38
44
 
39
- def _create_module_widget(
40
- self, module: db.Module, sync_receipt: SyncReceipt
41
- ) -> MemoryTreeWidgetItem:
45
+ def _create_module_widget(self, module: db.Module) -> MemoryTreeWidgetItem:
42
46
  module_widget = MemoryTreeWidgetItem(
43
47
  id=module.id, data=module, strings=[module.name]
44
48
  )
45
49
  module_widget.setFlags(Qt.ItemFlag.ItemIsEnabled)
46
-
47
- # Todo not sure if modules should get highlighted since they can't be unhighlighted by selecting them...
48
- if sync_receipt.was_updated(module):
49
- self.mark_as_unseen(module_widget)
50
+ module_widget.setIcon(0, icons.tree_items.module)
50
51
 
51
52
  return module_widget
52
53
 
53
54
  def _create_page_widget(
54
55
  self, page: db.ModulePage, sync_receipt: SyncReceipt
55
- ) -> MemoryTreeWidgetItem:
56
- page_widget = MemoryTreeWidgetItem(id=page.id, data=page, strings=[page.name])
56
+ ) -> TreeWidgetDataItem:
57
+ page_widget = TreeWidgetDataItem(id=page.id, data=page, strings=[page.name])
57
58
  page_widget.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable)
59
+ page_widget.setIcon(0, icons.tree_items.page)
58
60
 
59
61
  if sync_receipt.was_updated(page):
60
62
  self.mark_as_unseen(page_widget)
@@ -1,3 +1,4 @@
1
+ import html
1
2
  import logging
2
3
  from typing import Optional
3
4
 
@@ -6,12 +7,11 @@ from bs4 import BeautifulSoup, Tag
6
7
  from qasync import asyncSlot
7
8
  from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
8
9
  from qcanvas_backend.net.resources.extracting.no_extractor_error import NoExtractorError
9
- from qcanvas_backend.net.resources.scanning.resource_scanner import ResourceScanner
10
+ from qcanvas_backend.util import is_link_invisible
10
11
  from qtpy.QtCore import QUrl, Slot
11
12
  from qtpy.QtGui import QDesktopServices
12
13
  from qtpy.QtWidgets import QTextBrowser
13
14
 
14
- from qcanvas import icons
15
15
  from qcanvas.backend_connectors import FrontendResourceManager
16
16
  from qcanvas.util.html_cleaner import clean_up_html
17
17
  from qcanvas.util.qurl_util import file_url
@@ -19,22 +19,6 @@ from qcanvas.util.qurl_util import file_url
19
19
  _logger = logging.getLogger(__name__)
20
20
 
21
21
 
22
- # class _DarkListener(QObject):
23
- # theme_changed = Signal(str)
24
- #
25
- # def __init__(self):
26
- # super().__init__()
27
- #
28
- # self._thread = threading.Thread(target=darkdetect.listener, args=(self._emit,))
29
- # self._thread.daemon = True
30
- # self._thread.start()
31
- #
32
- # def _emit(self, theme: str) -> None:
33
- # self.theme_changed.emit(theme)
34
- #
35
- # _dark_listener = _DarkListener()
36
-
37
-
38
22
  class ResourceRichBrowser(QTextBrowser):
39
23
  def __init__(self, downloader: ResourceManager):
40
24
  super().__init__()
@@ -50,12 +34,6 @@ class ResourceRichBrowser(QTextBrowser):
50
34
  self._downloader.download_finished.connect(self._download_updated)
51
35
  self._downloader.download_failed.connect(self._download_updated)
52
36
 
53
- # _dark_listener.theme_changed.connect(self._theme_changed)
54
-
55
- # @Slot()
56
- # def _theme_changed(self, theme: str) -> None:
57
- # print(theme)
58
-
59
37
  def show_blank(self, completely_blank: bool = False) -> None:
60
38
  if completely_blank:
61
39
  self.clear()
@@ -91,8 +69,7 @@ class ResourceRichBrowser(QTextBrowser):
91
69
  extractor = self._extractors.extractor_for_tag(resource_link)
92
70
  resource_id = extractor.resource_id_from_tag(resource_link)
93
71
 
94
- # FIXME private method
95
- if ResourceScanner._is_link_invisible(resource_link):
72
+ if is_link_invisible(resource_link):
96
73
  _logger.debug("Found dead link for %s, removing", resource_id)
97
74
  resource_link.decompose()
98
75
  continue
@@ -102,50 +79,57 @@ class ResourceRichBrowser(QTextBrowser):
102
79
  )
103
80
  continue
104
81
 
105
- file_link_tag = self._create_resource_link_tag(doc, resource_id)
82
+ file_link_tag = self._create_resource_link_tag(
83
+ resource_id, resource_link.name == "img"
84
+ )
106
85
  resource_link.replace_with(file_link_tag)
107
86
  except NoExtractorError:
108
87
  pass
109
88
 
110
89
  return str(doc)
111
90
 
112
- def _create_resource_link_tag(self, doc: BeautifulSoup, resource_id: str) -> Tag:
91
+ def _create_resource_link_tag(self, resource_id: str, is_image: bool) -> Tag:
113
92
  resource = self._current_content_resources[resource_id]
114
93
 
115
- file_link_tag = doc.new_tag(
116
- "a",
117
- attrs={
118
- "href": f"data:{resource_id}",
119
- },
120
- )
121
-
122
- file_link_tag.append(self._file_icon_tag(doc, resource.download_state))
123
- file_link_tag.append("\N{NO-BREAK SPACE}" + resource.file_name)
124
-
125
- _logger.debug(str(file_link_tag))
126
-
127
- return file_link_tag
128
-
129
- def _file_icon_tag(
130
- self, document: BeautifulSoup, download_state: db.ResourceDownloadState
131
- ) -> Tag:
132
- return document.new_tag(
133
- "img",
134
- attrs={
135
- "src": self._download_state_icon(download_state),
136
- "style": "vertical-align:middle",
137
- "width": 18,
138
- },
139
- )
94
+ # todo not sure if this is a good idea or not
95
+ # if is_image and resource.download_state == db.ResourceDownloadState.DOWNLOADED:
96
+ # location = self._downloader.resource_download_location(resource)
97
+ #
98
+ # file_link_tag = doc.new_tag(
99
+ # "img",
100
+ # attrs={
101
+ # "source": location.absolute(),
102
+ # },
103
+ # )
104
+ # else:
105
+
106
+ return BeautifulSoup(
107
+ markup=f"""
108
+ <a href="data:{html.escape(resource_id)}" style="font-weight: normal">
109
+ <!--<table style="vertical-align: middle; border-collapse: collapse;">
110
+ <tr>
111
+ <td style="text-decoration: none;">-->
112
+ <img height="16" src="{html.escape(self._download_state_icon(resource.download_state))}"/>
113
+ <!--</td>
114
+ <td>-->
115
+ {html.escape(resource.file_name)}
116
+ <!--</td>
117
+ </tr>
118
+ </table>-->
119
+ </a>
120
+ """,
121
+ features="html.parser",
122
+ ).a
140
123
 
141
124
  def _download_state_icon(self, download_state: db.ResourceDownloadState) -> str:
125
+ icon_path = ":icons/universal/downloads"
142
126
  match download_state:
143
127
  case db.ResourceDownloadState.DOWNLOADED:
144
- return icons.file_downloaded
128
+ return f"{icon_path}/downloaded.svg"
145
129
  case db.ResourceDownloadState.NOT_DOWNLOADED:
146
- return icons.file_not_downloaded
130
+ return f"{icon_path}/not_downloaded.svg"
147
131
  case db.ResourceDownloadState.FAILED:
148
- return icons.file_download_failed
132
+ return f"{icon_path}/download_failed.svg"
149
133
  case _:
150
134
  raise ValueError()
151
135
 
@@ -174,4 +158,21 @@ class ResourceRichBrowser(QTextBrowser):
174
158
  @Slot(db.Resource)
175
159
  def _download_updated(self, resource: db.Resource) -> None:
176
160
  if self._content is not None and resource.id in self._current_content_resources:
161
+ # BANDAID FIX: In the following situation:
162
+ # - Download is started
163
+ # - Synchronisation is started
164
+ # - Download finishes AFTER the sync
165
+ # --> `resource` is NOT `self._current_content_resources[resource.id]`, because the sync will reload the
166
+ # resource from the DB, but the downloader will still only know about the old resource object.
167
+ # This causes resources not update their download state in the viewer. This line "fixes" that, but does NOT
168
+ # address the root cause. I think reloading the resource from the DB somewhere is the only true fix for this
169
+
170
+ if self._current_content_resources[resource.id] is not resource:
171
+ _logger.warning(
172
+ "Resource has diverged from current loaded data, applying bandaid fix"
173
+ )
174
+ self._current_content_resources[resource.id].download_state = (
175
+ resource.download_state
176
+ )
177
+
177
178
  self._show_page_content(self._content)
@@ -1 +1,11 @@
1
+ from typing import Protocol
2
+
3
+ import qcanvas_backend.database.types as db
4
+ from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
5
+
1
6
  date_strftime_format = "%A, %Y-%m-%d, %H:%M:%S"
7
+
8
+
9
+ # todo what the hell is the point of this?
10
+ class SupportsReload(Protocol):
11
+ def reload(self, course: db.Course, *, sync_receipt: SyncReceipt) -> None: ...
@@ -0,0 +1,22 @@
1
+ from typing import List, Optional
2
+
3
+ from qtpy.QtWidgets import QTreeWidgetItem
4
+
5
+ from qcanvas.ui.memory_tree import MemoryTreeWidgetItem
6
+
7
+
8
+ class TreeWidgetDataItem(QTreeWidgetItem):
9
+ def __init__(
10
+ self, id: str, data: Optional[object], strings: Optional[List[str]] = None
11
+ ):
12
+ super().__init__(strings)
13
+ # Still needs ID because it is used to reselect the item
14
+ self._id = id
15
+ self.extra_data = data
16
+
17
+ @property
18
+ def id(self) -> str:
19
+ return self._id
20
+
21
+
22
+ AnyTreeDataItem = TreeWidgetDataItem | MemoryTreeWidgetItem