qcanvas 1.1.0__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 (87) 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 +2164 -349
  39. qcanvas/icons/{file-downloaded.svg → universal/downloads/downloaded.svg} +4 -4
  40. qcanvas/icons/universal/tabs/assignments_new_content.svg +3 -0
  41. qcanvas/icons/universal/tabs/mail_new_content.svg +3 -0
  42. qcanvas/icons/universal/tabs/pages_new_content.svg +3 -0
  43. qcanvas/icons/universal/tree_items/semester.svg +108 -0
  44. qcanvas/ui/course_viewer/content_tree.py +20 -3
  45. qcanvas/ui/course_viewer/course_tree/__init__.py +1 -0
  46. qcanvas/ui/course_viewer/course_tree/_course_icon_generator.py +86 -0
  47. qcanvas/ui/course_viewer/{course_tree.py → course_tree/course_tree.py} +20 -6
  48. qcanvas/ui/course_viewer/course_viewer.py +80 -23
  49. qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +15 -14
  50. qcanvas/ui/course_viewer/tabs/file_tab/__init__.py +1 -0
  51. qcanvas/ui/course_viewer/tabs/file_tab/file_tab.py +46 -0
  52. qcanvas/ui/course_viewer/tabs/file_tab/file_tree.py +99 -0
  53. qcanvas/ui/course_viewer/tabs/file_tab/pages_file_tree.py +56 -0
  54. qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +11 -11
  55. qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +13 -11
  56. qcanvas/ui/course_viewer/tabs/resource_rich_browser.py +44 -52
  57. qcanvas/ui/course_viewer/tabs/util.py +10 -0
  58. qcanvas/ui/course_viewer/tree_widget_data_item.py +22 -0
  59. qcanvas/ui/main_ui/course_viewer_container.py +46 -3
  60. qcanvas/ui/main_ui/options/auto_download_resources_option.py +3 -1
  61. qcanvas/ui/main_ui/options/theme_selection_menu.py +2 -0
  62. qcanvas/ui/main_ui/qcanvas_window.py +17 -4
  63. qcanvas/ui/main_ui/status_bar_progress_display.py +17 -8
  64. qcanvas/ui/memory_tree/_tree_memory.py +1 -0
  65. qcanvas/ui/memory_tree/memory_tree_widget.py +2 -2
  66. qcanvas/ui/setup/setup_dialog.py +1 -1
  67. qcanvas/util/file_icons.py +54 -0
  68. qcanvas/util/html_cleaner.py +2 -0
  69. qcanvas/util/layouts.py +5 -2
  70. qcanvas/util/settings/_mapped_setting.py +6 -1
  71. qcanvas/util/themes/__init__.py +2 -0
  72. qcanvas/util/themes/_colour_scheme_helper.py +38 -0
  73. qcanvas/util/themes/_selected_theme.py +10 -0
  74. qcanvas/util/themes/_theme_changed_event.py +17 -0
  75. qcanvas/util/themes/_theme_changer.py +86 -0
  76. qcanvas/util/ui_tools.py +5 -1
  77. {qcanvas-1.1.0.dist-info → qcanvas-1.2.0.dist-info}/METADATA +15 -5
  78. qcanvas-1.2.0.dist-info/RECORD +118 -0
  79. qcanvas/icons/sync.svg +0 -7
  80. qcanvas/util/themes.py +0 -27
  81. qcanvas-1.1.0.dist-info/RECORD +0 -69
  82. /qcanvas/icons/{main_icon.svg → universal/branding/main_icon.svg} +0 -0
  83. /qcanvas/icons/{file-download-failed.svg → universal/downloads/download_failed.svg} +0 -0
  84. /qcanvas/icons/{file-not-downloaded.svg → universal/downloads/not_downloaded.svg} +0 -0
  85. /qcanvas/icons/{file-unknown.svg → universal/downloads/unknown.svg} +0 -0
  86. {qcanvas-1.1.0.dist-info → qcanvas-1.2.0.dist-info}/WHEEL +0 -0
  87. {qcanvas-1.1.0.dist-info → qcanvas-1.2.0.dist-info}/entry_points.txt +0 -0
@@ -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__()
@@ -85,8 +69,7 @@ class ResourceRichBrowser(QTextBrowser):
85
69
  extractor = self._extractors.extractor_for_tag(resource_link)
86
70
  resource_id = extractor.resource_id_from_tag(resource_link)
87
71
 
88
- # FIXME private method
89
- if ResourceScanner._is_link_invisible(resource_link):
72
+ if is_link_invisible(resource_link):
90
73
  _logger.debug("Found dead link for %s, removing", resource_id)
91
74
  resource_link.decompose()
92
75
  continue
@@ -97,7 +80,7 @@ class ResourceRichBrowser(QTextBrowser):
97
80
  continue
98
81
 
99
82
  file_link_tag = self._create_resource_link_tag(
100
- doc, resource_id, resource_link.name == "img"
83
+ resource_id, resource_link.name == "img"
101
84
  )
102
85
  resource_link.replace_with(file_link_tag)
103
86
  except NoExtractorError:
@@ -105,9 +88,7 @@ class ResourceRichBrowser(QTextBrowser):
105
88
 
106
89
  return str(doc)
107
90
 
108
- def _create_resource_link_tag(
109
- self, doc: BeautifulSoup, resource_id: str, is_image: bool
110
- ) -> Tag:
91
+ def _create_resource_link_tag(self, resource_id: str, is_image: bool) -> Tag:
111
92
  resource = self._current_content_resources[resource_id]
112
93
 
113
94
  # todo not sure if this is a good idea or not
@@ -121,40 +102,34 @@ class ResourceRichBrowser(QTextBrowser):
121
102
  # },
122
103
  # )
123
104
  # else:
124
- file_link_tag = doc.new_tag(
125
- "a",
126
- attrs={
127
- "href": f"data:{resource_id}",
128
- },
129
- )
130
-
131
- file_link_tag.append(self._file_icon_tag(doc, resource.download_state))
132
- file_link_tag.append("\N{NO-BREAK SPACE}" + resource.file_name)
133
-
134
- _logger.debug(str(file_link_tag))
135
-
136
- return file_link_tag
137
-
138
- def _file_icon_tag(
139
- self, document: BeautifulSoup, download_state: db.ResourceDownloadState
140
- ) -> Tag:
141
- return document.new_tag(
142
- "img",
143
- attrs={
144
- "src": self._download_state_icon(download_state),
145
- "style": "vertical-align:middle",
146
- "width": 18,
147
- },
148
- )
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
149
123
 
150
124
  def _download_state_icon(self, download_state: db.ResourceDownloadState) -> str:
125
+ icon_path = ":icons/universal/downloads"
151
126
  match download_state:
152
127
  case db.ResourceDownloadState.DOWNLOADED:
153
- return icons.file_downloaded
128
+ return f"{icon_path}/downloaded.svg"
154
129
  case db.ResourceDownloadState.NOT_DOWNLOADED:
155
- return icons.file_not_downloaded
130
+ return f"{icon_path}/not_downloaded.svg"
156
131
  case db.ResourceDownloadState.FAILED:
157
- return icons.file_download_failed
132
+ return f"{icon_path}/download_failed.svg"
158
133
  case _:
159
134
  raise ValueError()
160
135
 
@@ -183,4 +158,21 @@ class ResourceRichBrowser(QTextBrowser):
183
158
  @Slot(db.Resource)
184
159
  def _download_updated(self, resource: db.Resource) -> None:
185
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
+
186
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
@@ -1,30 +1,68 @@
1
1
  import logging
2
+ from math import floor
2
3
  from typing import *
3
4
 
4
5
  import qcanvas_backend.database.types as db
5
6
  from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
6
7
  from qcanvas_backend.net.sync.sync_receipt import SyncReceipt, empty_receipt
7
- from qtpy.QtCore import Qt
8
+ from qtpy.QtCore import Qt, Slot
8
9
  from qtpy.QtWidgets import *
9
10
 
11
+ from qcanvas import icons
10
12
  from qcanvas.ui.course_viewer.course_viewer import CourseViewer
13
+ from qcanvas.util import themes
11
14
 
12
15
  _logger = logging.getLogger(__name__)
13
16
 
14
17
 
18
+ class _PlaceholderLogo(QLabel):
19
+ """
20
+ Automatically resizing logo icon for when no course is selected
21
+ """
22
+
23
+ def __init__(self):
24
+ super().__init__()
25
+ self._icon = icons.branding.logo_transparent
26
+ self._old_width = -1
27
+ self._old_height = -1
28
+ self.setAlignment(Qt.AlignmentFlag.AlignCenter)
29
+ self.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Ignored)
30
+ # Because we are using a pixmap for the icon, it will not get updated like a normal QIcon when the theme changes,
31
+ # So we need to update it ourselves
32
+ themes.theme_changed().connect(self._theme_changed)
33
+
34
+ def resizeEvent(self, event) -> None:
35
+ self._update_image()
36
+
37
+ @Slot()
38
+ def _theme_changed(self) -> None:
39
+ self._update_image(force=True)
40
+
41
+ def _update_image(self, force: bool = False) -> None:
42
+ # Calculate the size of the logo as half of the width/height with a max size of 1000x1000
43
+ width = min(floor(self.width() * 0.5), 500)
44
+ height = min(floor(self.height() * 0.5), 500)
45
+
46
+ if force or (width != self._old_width and height != self._old_height):
47
+ self._old_width = width
48
+ self._old_height = height
49
+ self.setPixmap(self._icon.pixmap(width, height))
50
+
51
+
15
52
  class CourseViewerContainer(QStackedWidget):
16
53
  def __init__(self, downloader: ResourceManager):
17
54
  super().__init__()
18
55
  self._course_viewers: dict[str, CourseViewer] = {}
19
56
  self._downloader = downloader
20
57
  self._last_course_id: Optional[str] = None
58
+ self._selected_course: Optional[db.Course] = None
21
59
  self._last_sync_receipt: SyncReceipt = empty_receipt()
22
- self._placeholder = QLabel("No Course Selected")
23
- self._placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
60
+ self._placeholder = _PlaceholderLogo()
24
61
  self.addWidget(self._placeholder)
25
62
 
26
63
  def show_blank(self) -> None:
27
64
  self._last_course_id = None
65
+ self._selected_course = None
28
66
  self.setCurrentWidget(self._placeholder)
29
67
 
30
68
  def load_course(self, course: db.Course) -> None:
@@ -40,6 +78,7 @@ class CourseViewerContainer(QStackedWidget):
40
78
  viewer = self._course_viewers[course.id]
41
79
 
42
80
  self.setCurrentWidget(viewer)
81
+ self._selected_course = course
43
82
  self._last_course_id = course.id
44
83
 
45
84
  async def reload_all(
@@ -50,3 +89,7 @@ class CourseViewerContainer(QStackedWidget):
50
89
  if course.id in self._course_viewers:
51
90
  viewer = self._course_viewers[course.id]
52
91
  viewer.reload(course, sync_receipt=sync_receipt)
92
+
93
+ @property
94
+ def selected_course(self) -> Optional[db.Course]:
95
+ return self._selected_course
@@ -5,6 +5,7 @@ from qtpy.QtCore import Slot
5
5
  from qtpy.QtGui import QAction
6
6
  from qtpy.QtWidgets import QMenu
7
7
 
8
+ from qcanvas import icons
8
9
  from qcanvas.util import settings
9
10
 
10
11
  _logger = logging.getLogger(__name__)
@@ -36,6 +37,7 @@ class _EnableVideoDownloadOption(QAction):
36
37
 
37
38
  class AutoDownloadResourcesMenu(QMenu):
38
39
  def __init__(self, parent: Optional[QMenu] = None):
39
- super().__init__("Download new resources", parent)
40
+ super().__init__("Auto download resources", parent)
40
41
  self.addAction(_EnableAutoDownloadOption(self))
41
42
  self.addAction(_EnableVideoDownloadOption(self))
43
+ self.setIcon(icons.options.auto_download)
@@ -4,6 +4,7 @@ from qtpy.QtCore import Slot
4
4
  from qtpy.QtGui import QAction, QActionGroup
5
5
  from qtpy.QtWidgets import QMenu
6
6
 
7
+ from qcanvas import icons
7
8
  from qcanvas.util import settings, themes
8
9
 
9
10
  _logger = logging.getLogger(__name__)
@@ -38,6 +39,7 @@ class ThemeSelectionMenu(QMenu):
38
39
  actions = [auto_theme, light_theme, dark_theme, native_theme]
39
40
 
40
41
  self.addActions(actions)
42
+ self.setIcon(icons.options.theme)
41
43
 
42
44
  for theme in actions:
43
45
  action_group.addAction(theme)
@@ -9,7 +9,7 @@ from qcanvas_backend.database.data_monolith import DataMonolith
9
9
  from qcanvas_backend.net.sync.sync_receipt import SyncReceipt, empty_receipt
10
10
  from qcanvas_backend.qcanvas import QCanvas
11
11
  from qtpy.QtCore import QUrl, Signal, Slot
12
- from qtpy.QtGui import QDesktopServices, QIcon, QKeySequence
12
+ from qtpy.QtGui import QDesktopServices, QKeySequence
13
13
  from qtpy.QtWidgets import *
14
14
 
15
15
  from qcanvas import icons
@@ -37,7 +37,7 @@ class QCanvasWindow(QMainWindow):
37
37
  super().__init__()
38
38
 
39
39
  self.setWindowTitle("QCanvas")
40
- self.setWindowIcon(QIcon(icons.main_icon))
40
+ self.setWindowIcon(icons.branding.main_icon)
41
41
 
42
42
  self._operation_semaphore = BoundedSemaphore()
43
43
  self._data: Optional[DataMonolith] = None
@@ -74,6 +74,7 @@ class QCanvasWindow(QMainWindow):
74
74
  shortcut=QKeySequence("Ctrl+S"),
75
75
  triggered=self._synchronise_requested,
76
76
  parent=app_menu,
77
+ icon=icons.actions.sync,
77
78
  )
78
79
 
79
80
  create_qaction(
@@ -81,17 +82,22 @@ class QCanvasWindow(QMainWindow):
81
82
  shortcut=QKeySequence("Ctrl+D"),
82
83
  triggered=self._open_downloads_folder,
83
84
  parent=app_menu,
85
+ icon=icons.actions.open_downloads,
84
86
  )
85
87
 
86
88
  create_qaction(
87
- name="Quick canvas login",
89
+ name="Open Canvas in browser",
88
90
  shortcut=QKeySequence("Ctrl+O"),
89
91
  triggered=self._open_quick_auth_in_browser,
90
92
  parent=app_menu,
93
+ icon=icons.actions.quick_login,
91
94
  )
92
95
 
93
96
  create_qaction(
94
- name="Mark all as seen", triggered=self._clear_new_items, parent=app_menu
97
+ name="Mark all as seen",
98
+ triggered=self._clear_new_items,
99
+ parent=app_menu,
100
+ icon=icons.actions.mark_all_read,
95
101
  )
96
102
 
97
103
  create_qaction(
@@ -99,6 +105,7 @@ class QCanvasWindow(QMainWindow):
99
105
  shortcut=QKeySequence("Ctrl+Q"),
100
106
  triggered=lambda: self.close(),
101
107
  parent=app_menu,
108
+ icon=icons.actions.exit,
102
109
  )
103
110
 
104
111
  options_menu = menu_bar.addMenu("Options")
@@ -229,6 +236,12 @@ class QCanvasWindow(QMainWindow):
229
236
  @Slot()
230
237
  def _open_downloads_folder(self) -> None:
231
238
  directory = self._qcanvas.resource_manager.downloads_folder
239
+
240
+ if self._course_viewer_container.selected_course is not None:
241
+ directory /= self._qcanvas.resource_manager.course_folder_name(
242
+ self._course_viewer_container.selected_course
243
+ )
244
+
232
245
  directory.mkdir(parents=True, exist_ok=True)
233
246
 
234
247
  QDesktopServices.openUrl(file_url(directory))