qcanvas 1.0.11__py3-none-any.whl → 2026.1.19__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qcanvas/__init__.py +60 -0
- qcanvas/app.py +72 -0
- qcanvas/backend_connectors/frontend_resource_manager.py +13 -5
- qcanvas/backend_connectors/qcanvas_task_master.py +2 -2
- qcanvas/icons/__init__.py +55 -6
- qcanvas/icons/_icon_type.py +42 -0
- qcanvas/icons/icons.qrc +48 -8
- qcanvas/icons/rc_icons.py +2477 -566
- qcanvas/settings/__init__.py +6 -0
- qcanvas/{util/settings → settings}/_client_settings.py +15 -6
- qcanvas/settings/_course_settings.py +54 -0
- qcanvas/{util/settings → settings}/_mapped_setting.py +8 -6
- qcanvas/{util/settings → settings}/_ui_settings.py +5 -5
- qcanvas/theme.py +101 -0
- qcanvas/ui/course_viewer/content_tree.py +37 -19
- qcanvas/ui/course_viewer/course_tree/__init__.py +1 -0
- qcanvas/ui/course_viewer/course_tree/_course_icon_generator.py +86 -0
- qcanvas/ui/course_viewer/{course_tree.py → course_tree/course_tree.py} +29 -14
- qcanvas/ui/course_viewer/course_viewer.py +79 -46
- qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tab.py +107 -29
- qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +19 -18
- qcanvas/ui/course_viewer/tabs/content_tab.py +33 -39
- qcanvas/ui/course_viewer/tabs/file_tab/__init__.py +1 -0
- qcanvas/ui/course_viewer/tabs/file_tab/file_tab.py +46 -0
- qcanvas/ui/course_viewer/tabs/file_tab/file_tree.py +96 -0
- qcanvas/ui/course_viewer/tabs/file_tab/pages_file_tree.py +55 -0
- qcanvas/ui/course_viewer/tabs/mail_tab/mail_tab.py +50 -27
- qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +18 -19
- qcanvas/ui/course_viewer/tabs/page_tab/page_tab.py +3 -3
- qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +18 -16
- qcanvas/ui/course_viewer/tabs/resource_rich_browser.py +61 -74
- qcanvas/ui/course_viewer/tree_widget_data_item.py +22 -0
- qcanvas/ui/memory_tree/_tree_memory.py +45 -41
- qcanvas/ui/memory_tree/memory_tree_widget.py +22 -18
- qcanvas/ui/memory_tree/memory_tree_widget_item.py +3 -3
- qcanvas/ui/qcanvas_window/__init__.py +1 -0
- qcanvas/ui/qcanvas_window/course_viewer_container.py +95 -0
- qcanvas/ui/{main_ui → qcanvas_window}/options/auto_download_resources_option.py +8 -6
- qcanvas/ui/{main_ui → qcanvas_window}/options/quick_sync_option.py +7 -6
- qcanvas/ui/{main_ui → qcanvas_window}/options/sync_on_start_option.py +7 -6
- qcanvas/ui/{main_ui → qcanvas_window}/options/theme_selection_menu.py +12 -10
- qcanvas/ui/{main_ui → qcanvas_window}/qcanvas_window.py +74 -45
- qcanvas/ui/{main_ui → qcanvas_window}/status_bar_progress_display.py +20 -12
- qcanvas/ui/qml_components/__init__.py +4 -0
- qcanvas/ui/qml_components/attachments_pane.py +70 -0
- qcanvas/ui/qml_components/comments_pane.py +83 -0
- qcanvas/ui/qml_components/qml/AttachmentsList.ui.qml +15 -0
- qcanvas/ui/qml_components/qml/AttachmentsListDelegate.qml +77 -0
- qcanvas/ui/qml_components/qml/AttachmentsListModel.qml +19 -0
- qcanvas/ui/qml_components/qml/AttachmentsPane.qml +11 -0
- qcanvas/ui/qml_components/qml/CommentsList.ui.qml +15 -0
- qcanvas/ui/qml_components/qml/CommentsListDelegate.ui.qml +118 -0
- qcanvas/ui/qml_components/qml/CommentsListModel.qml +56 -0
- qcanvas/ui/qml_components/qml/CommentsPane.qml +11 -0
- qcanvas/ui/qml_components/qml/DecoratedText.ui.qml +44 -0
- qcanvas/ui/qml_components/qml/Spacer.ui.qml +7 -0
- qcanvas/ui/qml_components/qml/ThemedRectangle.qml +53 -0
- qcanvas/ui/qml_components/qml/__init__.py +3 -0
- qcanvas/ui/qml_components/qml/rc_qml.py +709 -0
- qcanvas/ui/qml_components/qml/rc_qml.qrc +16 -0
- qcanvas/ui/qml_components/qml_bridge_types.py +95 -0
- qcanvas/ui/qml_components/qml_pane.py +21 -0
- qcanvas/ui/setup/setup_checker.py +3 -3
- qcanvas/ui/setup/setup_dialog.py +173 -80
- qcanvas/util/__init__.py +0 -2
- qcanvas/util/auto_downloader.py +9 -8
- qcanvas/util/basic_fonts.py +2 -2
- qcanvas/util/context_dict.py +12 -0
- qcanvas/util/file_icons.py +46 -0
- qcanvas/util/html_cleaner.py +2 -0
- qcanvas/util/layouts.py +9 -8
- qcanvas/util/paths.py +26 -22
- qcanvas/util/qurl_util.py +1 -1
- qcanvas/util/runtime.py +20 -0
- qcanvas/util/ui_tools.py +121 -7
- qcanvas/util/url_checker.py +1 -1
- qcanvas-2026.1.19.dist-info/METADATA +95 -0
- qcanvas-2026.1.19.dist-info/RECORD +92 -0
- {qcanvas-1.0.11.dist-info → qcanvas-2026.1.19.dist-info}/WHEEL +1 -1
- qcanvas-2026.1.19.dist-info/entry_points.txt +3 -0
- qcanvas/app_start/__init__.py +0 -54
- qcanvas/icons/file-download-failed.svg +0 -6
- qcanvas/icons/file-downloaded.svg +0 -6
- qcanvas/icons/file-not-downloaded.svg +0 -6
- qcanvas/icons/file-unknown.svg +0 -6
- qcanvas/icons/main_icon.svg +0 -325
- qcanvas/icons/sync.svg +0 -7
- qcanvas/run.py +0 -30
- qcanvas/ui/main_ui/__init__.py +0 -0
- qcanvas/ui/main_ui/course_viewer_container.py +0 -52
- qcanvas/util/settings/__init__.py +0 -9
- qcanvas/util/themes.py +0 -24
- qcanvas-1.0.11.dist-info/METADATA +0 -61
- qcanvas-1.0.11.dist-info/RECORD +0 -68
- qcanvas-1.0.11.dist-info/entry_points.txt +0 -3
- /qcanvas/ui/course_viewer/tabs/{util.py → constants.py} +0 -0
- /qcanvas/ui/{main_ui → qcanvas_window}/options/__init__.py +0 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from abc import ABC
|
|
3
|
+
|
|
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
|
+
|
|
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
|
+
class FileTree(ContentTree[db.Course], ABC):
|
|
23
|
+
@classmethod
|
|
24
|
+
def create_from_receipt(
|
|
25
|
+
cls,
|
|
26
|
+
course: db.Course,
|
|
27
|
+
*,
|
|
28
|
+
sync_receipt: SyncReceipt,
|
|
29
|
+
resource_manager: ResourceManager,
|
|
30
|
+
) -> "FileTree":
|
|
31
|
+
tree = cls(tree_name=course.id, resource_manager=resource_manager)
|
|
32
|
+
tree.reload(course, sync_receipt=sync_receipt)
|
|
33
|
+
return tree
|
|
34
|
+
|
|
35
|
+
def __init__(self, tree_name: str, *, resource_manager: ResourceManager):
|
|
36
|
+
super().__init__(tree_name, emit_selection_signal_for_type=object)
|
|
37
|
+
self._resource_manager = resource_manager
|
|
38
|
+
|
|
39
|
+
self.ui_setup(header_text=["File", "Date"])
|
|
40
|
+
self.set_columns_resize_mode(
|
|
41
|
+
[QHeaderView.ResizeMode.Stretch, QHeaderView.ResizeMode.ResizeToContents]
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
45
|
+
self.customContextMenuRequested.connect(self._context_menu)
|
|
46
|
+
|
|
47
|
+
def _create_group_widget(
|
|
48
|
+
self, group: db.ContentGroup, sync_receipt: SyncReceipt
|
|
49
|
+
) -> MemoryTreeWidgetItem:
|
|
50
|
+
group_widget = MemoryTreeWidgetItem(
|
|
51
|
+
id=group.id, data=group, strings=[group.name]
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
group_widget.setFlags(Qt.ItemFlag.ItemIsEnabled)
|
|
55
|
+
|
|
56
|
+
if sync_receipt.was_updated(group):
|
|
57
|
+
self.mark_as_unseen(group_widget)
|
|
58
|
+
|
|
59
|
+
return group_widget
|
|
60
|
+
|
|
61
|
+
def _create_resource_widget(
|
|
62
|
+
self, resource: db.Resource, sync_receipt: SyncReceipt
|
|
63
|
+
) -> QTreeWidgetItem:
|
|
64
|
+
# fixme the reesource widget items shouls NOT be a memory widget item because they can't be collapsed, but
|
|
65
|
+
# mostly because the same file can appear in the tree multiple times in different places, which memory tree
|
|
66
|
+
# can NOT deal with!
|
|
67
|
+
item_widget = TreeWidgetDataItem(
|
|
68
|
+
id=resource.id,
|
|
69
|
+
data=resource,
|
|
70
|
+
strings=[resource.file_name, str(resource.discovery_date.date())],
|
|
71
|
+
)
|
|
72
|
+
item_widget.setIcon(
|
|
73
|
+
0,
|
|
74
|
+
icon_for_filename(resource.file_name),
|
|
75
|
+
)
|
|
76
|
+
item_widget.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable)
|
|
77
|
+
|
|
78
|
+
if sync_receipt.was_updated(resource):
|
|
79
|
+
self.mark_as_unseen(item_widget)
|
|
80
|
+
|
|
81
|
+
return item_widget
|
|
82
|
+
|
|
83
|
+
@Slot(QPoint)
|
|
84
|
+
def _context_menu(self, point: QPoint) -> None:
|
|
85
|
+
item = self.itemAt(point)
|
|
86
|
+
|
|
87
|
+
if isinstance(item, AnyTreeDataItem):
|
|
88
|
+
menu = QMenu()
|
|
89
|
+
create_qaction(
|
|
90
|
+
name="Test",
|
|
91
|
+
parent=menu,
|
|
92
|
+
triggered=lambda: print(f"Clicked {item.extra_data.file_name}"),
|
|
93
|
+
)
|
|
94
|
+
menu.addAction("Another thing")
|
|
95
|
+
|
|
96
|
+
menu.exec(self.mapToGlobal(point))
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Sequence
|
|
3
|
+
|
|
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
|
+
|
|
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
|
+
def __init__(self, tree_name: str, *, resource_manager: ResourceManager):
|
|
16
|
+
super().__init__(
|
|
17
|
+
tree_name=f"{tree_name}.pages", resource_manager=resource_manager
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def create_tree_items(
|
|
21
|
+
self, data: db.Course, sync_receipt: SyncReceipt
|
|
22
|
+
) -> Sequence[MemoryTreeWidgetItem]:
|
|
23
|
+
widgets = []
|
|
24
|
+
|
|
25
|
+
for group in data.modules: # type: db.Module
|
|
26
|
+
if not group.pages:
|
|
27
|
+
continue
|
|
28
|
+
|
|
29
|
+
# Init group_widget lazily to prevent creating pointless tree widgets
|
|
30
|
+
group_widget: MemoryTreeWidgetItem | None = None
|
|
31
|
+
items_in_group = set()
|
|
32
|
+
|
|
33
|
+
for item in group.pages:
|
|
34
|
+
resource_widgets = []
|
|
35
|
+
|
|
36
|
+
for resource in item.resources: # type: db.Resource
|
|
37
|
+
if resource.id not in items_in_group:
|
|
38
|
+
items_in_group.add(resource.id)
|
|
39
|
+
|
|
40
|
+
if group_widget is None:
|
|
41
|
+
group_widget = self._create_group_widget(
|
|
42
|
+
group, sync_receipt
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
resource_widgets.append(
|
|
46
|
+
self._create_resource_widget(resource, sync_receipt)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if len(resource_widgets) > 0:
|
|
50
|
+
group_widget.addChildren(resource_widgets)
|
|
51
|
+
|
|
52
|
+
if group_widget is not None:
|
|
53
|
+
widgets.append(group_widget)
|
|
54
|
+
|
|
55
|
+
return widgets
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import override
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
from
|
|
6
|
-
from
|
|
7
|
-
from
|
|
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.
|
|
12
|
-
|
|
13
|
-
from qcanvas.
|
|
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:
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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.
|
|
58
|
-
self._date_sent_label.setText(
|
|
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,53 +1,52 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Sequence
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
from
|
|
6
|
-
from
|
|
4
|
+
from libqcanvas import db
|
|
5
|
+
from libqcanvas.net.sync.sync_receipt import SyncReceipt
|
|
6
|
+
from PySide6.QtWidgets import QHeaderView
|
|
7
7
|
|
|
8
|
+
from qcanvas import icons
|
|
8
9
|
from qcanvas.ui.course_viewer.content_tree import ContentTree
|
|
9
|
-
from qcanvas.ui.
|
|
10
|
+
from qcanvas.ui.course_viewer.tree_widget_data_item import TreeWidgetDataItem
|
|
10
11
|
|
|
11
12
|
_logger = logging.getLogger(__name__)
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class MailTree(ContentTree[db.Course]):
|
|
15
|
-
|
|
16
16
|
def __init__(self, course_id: str):
|
|
17
17
|
super().__init__(
|
|
18
18
|
tree_name=f"course.{course_id}.mail",
|
|
19
|
-
emit_selection_signal_for_type=db.
|
|
19
|
+
emit_selection_signal_for_type=db.Message,
|
|
20
20
|
)
|
|
21
|
+
|
|
21
22
|
self.ui_setup(
|
|
22
23
|
header_text=["Subject", "Sender"],
|
|
23
24
|
max_width=500,
|
|
24
25
|
min_width=300,
|
|
25
26
|
indentation=20,
|
|
27
|
+
alternating_row_colours=True,
|
|
26
28
|
)
|
|
27
29
|
|
|
28
|
-
self.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
header = self.header()
|
|
32
|
-
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
|
33
|
-
header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
|
|
34
|
-
header.setStretchLastSection(False)
|
|
30
|
+
self.set_columns_resize_mode(
|
|
31
|
+
[QHeaderView.ResizeMode.Stretch, QHeaderView.ResizeMode.ResizeToContents]
|
|
32
|
+
)
|
|
35
33
|
|
|
36
34
|
def create_tree_items(
|
|
37
35
|
self, course: db.Course, sync_receipt: SyncReceipt
|
|
38
|
-
) -> Sequence[
|
|
36
|
+
) -> Sequence[TreeWidgetDataItem]:
|
|
39
37
|
widgets = []
|
|
40
38
|
|
|
41
|
-
for message in course.messages: # type: db.
|
|
39
|
+
for message in course.messages: # type: db.Message
|
|
42
40
|
message_widget = self._create_mail_widget(message, sync_receipt)
|
|
41
|
+
message_widget.setIcon(0, icons.tree_items.mail)
|
|
43
42
|
widgets.append(message_widget)
|
|
44
43
|
|
|
45
44
|
return widgets
|
|
46
45
|
|
|
47
46
|
def _create_mail_widget(
|
|
48
|
-
self, message: db.
|
|
49
|
-
) ->
|
|
50
|
-
message_widget =
|
|
47
|
+
self, message: db.Message, sync_receipt: SyncReceipt
|
|
48
|
+
) -> TreeWidgetDataItem:
|
|
49
|
+
message_widget = TreeWidgetDataItem(
|
|
51
50
|
id=message.id,
|
|
52
51
|
data=message,
|
|
53
52
|
strings=[message.name, message.sender_name],
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
from
|
|
5
|
-
from
|
|
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,11 +1,13 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Sequence
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
from
|
|
6
|
-
from
|
|
4
|
+
from libqcanvas import db
|
|
5
|
+
from libqcanvas.net.sync.sync_receipt import SyncReceipt
|
|
6
|
+
from PySide6.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__)
|
|
@@ -15,8 +17,9 @@ class PageTree(ContentTree[db.Course]):
|
|
|
15
17
|
def __init__(self, course_id: str):
|
|
16
18
|
super().__init__(
|
|
17
19
|
tree_name=f"course.{course_id}.modules",
|
|
18
|
-
emit_selection_signal_for_type=db.
|
|
20
|
+
emit_selection_signal_for_type=db.Page,
|
|
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
|
-
|
|
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
|
-
self, page: db.
|
|
55
|
-
) ->
|
|
56
|
-
page_widget =
|
|
55
|
+
self, page: db.Page, sync_receipt: SyncReceipt
|
|
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,42 +1,24 @@
|
|
|
1
|
+
import html
|
|
1
2
|
import logging
|
|
2
3
|
from typing import Optional
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
from libqcanvas import db
|
|
5
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
|
|
6
12
|
from qasync import asyncSlot
|
|
7
|
-
|
|
8
|
-
from qcanvas_backend.net.resources.extracting.no_extractor_error import NoExtractorError
|
|
9
|
-
from qcanvas_backend.net.resources.scanning.resource_scanner import ResourceScanner
|
|
10
|
-
from qtpy.QtCore import QUrl, Slot
|
|
11
|
-
from qtpy.QtGui import QDesktopServices
|
|
12
|
-
from qtpy.QtWidgets import QTextBrowser
|
|
13
|
-
|
|
14
|
-
from qcanvas import icons
|
|
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
|
-
# 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
20
|
class ResourceRichBrowser(QTextBrowser):
|
|
39
|
-
def __init__(self, downloader:
|
|
21
|
+
def __init__(self, downloader: FrontendResourceManager):
|
|
40
22
|
super().__init__()
|
|
41
23
|
self._downloader = downloader
|
|
42
24
|
self._content: Optional[db.CourseContentItem] = None
|
|
@@ -46,15 +28,8 @@ class ResourceRichBrowser(QTextBrowser):
|
|
|
46
28
|
self.setOpenLinks(False)
|
|
47
29
|
self.anchorClicked.connect(self._open_url)
|
|
48
30
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
self._downloader.download_failed.connect(self._download_updated)
|
|
52
|
-
|
|
53
|
-
# _dark_listener.theme_changed.connect(self._theme_changed)
|
|
54
|
-
|
|
55
|
-
# @Slot()
|
|
56
|
-
# def _theme_changed(self, theme: str) -> None:
|
|
57
|
-
# print(theme)
|
|
31
|
+
self._downloader.download_finished.connect(self._download_updated)
|
|
32
|
+
self._downloader.download_failed.connect(self._download_updated)
|
|
58
33
|
|
|
59
34
|
def show_blank(self, completely_blank: bool = False) -> None:
|
|
60
35
|
if completely_blank:
|
|
@@ -65,14 +40,14 @@ class ResourceRichBrowser(QTextBrowser):
|
|
|
65
40
|
self._content = None
|
|
66
41
|
self._current_content_resources.clear()
|
|
67
42
|
|
|
68
|
-
def show_content(self, page: db.
|
|
43
|
+
def show_content(self, page: db.AnyContentItem) -> None:
|
|
69
44
|
if page.body is None:
|
|
70
45
|
self.show_blank()
|
|
71
46
|
else:
|
|
72
47
|
self._collect_resources(page)
|
|
73
48
|
self._show_page_content(page)
|
|
74
49
|
|
|
75
|
-
def _collect_resources(self, page: db.
|
|
50
|
+
def _collect_resources(self, page: db.AnyContentItem):
|
|
76
51
|
self._current_content_resources = {
|
|
77
52
|
resource.id: resource for resource in page.resources
|
|
78
53
|
}
|
|
@@ -91,8 +66,7 @@ class ResourceRichBrowser(QTextBrowser):
|
|
|
91
66
|
extractor = self._extractors.extractor_for_tag(resource_link)
|
|
92
67
|
resource_id = extractor.resource_id_from_tag(resource_link)
|
|
93
68
|
|
|
94
|
-
|
|
95
|
-
if ResourceScanner._is_link_invisible(resource_link):
|
|
69
|
+
if is_link_invisible(resource_link):
|
|
96
70
|
_logger.debug("Found dead link for %s, removing", resource_id)
|
|
97
71
|
resource_link.decompose()
|
|
98
72
|
continue
|
|
@@ -102,50 +76,49 @@ class ResourceRichBrowser(QTextBrowser):
|
|
|
102
76
|
)
|
|
103
77
|
continue
|
|
104
78
|
|
|
105
|
-
file_link_tag = self._create_resource_link_tag(
|
|
79
|
+
file_link_tag = self._create_resource_link_tag(
|
|
80
|
+
resource_id, resource_link.name == "img"
|
|
81
|
+
)
|
|
106
82
|
resource_link.replace_with(file_link_tag)
|
|
107
83
|
except NoExtractorError:
|
|
108
84
|
pass
|
|
109
85
|
|
|
110
86
|
return str(doc)
|
|
111
87
|
|
|
112
|
-
def _create_resource_link_tag(self,
|
|
88
|
+
def _create_resource_link_tag(self, resource_id: str, is_image: bool) -> Tag:
|
|
113
89
|
resource = self._current_content_resources[resource_id]
|
|
114
90
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
"
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
"style": "vertical-align:middle",
|
|
137
|
-
"width": 18,
|
|
138
|
-
},
|
|
139
|
-
)
|
|
91
|
+
# todo not sure if this is a good idea or not
|
|
92
|
+
# if is_image and resource.download_state == db.ResourceDownloadState.DOWNLOADED:
|
|
93
|
+
# location = self._downloader.resource_download_location(resource)
|
|
94
|
+
#
|
|
95
|
+
# file_link_tag = doc.new_tag(
|
|
96
|
+
# "img",
|
|
97
|
+
# attrs={
|
|
98
|
+
# "source": location.absolute(),
|
|
99
|
+
# },
|
|
100
|
+
# )
|
|
101
|
+
# else:
|
|
102
|
+
|
|
103
|
+
return BeautifulSoup(
|
|
104
|
+
markup=f"""
|
|
105
|
+
<a href="data:{html.escape(resource_id)}" style="font-weight: normal;">
|
|
106
|
+
<img height="18" src="{html.escape(self._download_state_icon(resource.download_state))}"/>
|
|
107
|
+
{html.escape(resource.file_name)}
|
|
108
|
+
</a>
|
|
109
|
+
""",
|
|
110
|
+
features="html.parser",
|
|
111
|
+
).a
|
|
140
112
|
|
|
141
113
|
def _download_state_icon(self, download_state: db.ResourceDownloadState) -> str:
|
|
114
|
+
icon_path = ":icons/universal/downloads"
|
|
142
115
|
match download_state:
|
|
143
116
|
case db.ResourceDownloadState.DOWNLOADED:
|
|
144
|
-
return
|
|
117
|
+
return f"{icon_path}/downloaded.svg"
|
|
145
118
|
case db.ResourceDownloadState.NOT_DOWNLOADED:
|
|
146
|
-
return
|
|
119
|
+
return f"{icon_path}/not_downloaded.svg"
|
|
147
120
|
case db.ResourceDownloadState.FAILED:
|
|
148
|
-
return
|
|
121
|
+
return f"{icon_path}/download_failed.svg"
|
|
149
122
|
case _:
|
|
150
123
|
raise ValueError()
|
|
151
124
|
|
|
@@ -161,17 +134,31 @@ class ResourceRichBrowser(QTextBrowser):
|
|
|
161
134
|
resource = self._current_content_resources[resource_id]
|
|
162
135
|
|
|
163
136
|
try:
|
|
164
|
-
await self._downloader.
|
|
137
|
+
await self._downloader.download_and_open(resource)
|
|
165
138
|
except Exception as e:
|
|
166
139
|
_logger.warning(
|
|
167
140
|
"Download of resource id=%s failed", resource_id, exc_info=e
|
|
168
141
|
)
|
|
169
142
|
return
|
|
170
143
|
|
|
171
|
-
resource_path = file_url(self._downloader.resource_download_location(resource))
|
|
172
|
-
QDesktopServices.openUrl(resource_path)
|
|
173
|
-
|
|
174
144
|
@Slot(db.Resource)
|
|
175
145
|
def _download_updated(self, resource: db.Resource) -> None:
|
|
176
146
|
if self._content is not None and resource.id in self._current_content_resources:
|
|
147
|
+
# BANDAID FIX: In the following situation:
|
|
148
|
+
# - Download is started
|
|
149
|
+
# - Synchronisation is started
|
|
150
|
+
# - Download finishes AFTER the sync
|
|
151
|
+
# --> `resource` is NOT `self._current_content_resources[resource.id]`, because the sync will reload the
|
|
152
|
+
# resource from the DB, but the downloader will still only know about the old resource object.
|
|
153
|
+
# This causes resources not update their download state in the viewer. This line "fixes" that, but does NOT
|
|
154
|
+
# address the root cause. I think reloading the resource from the DB somewhere is the only true fix for this
|
|
155
|
+
|
|
156
|
+
if self._current_content_resources[resource.id] is not resource:
|
|
157
|
+
_logger.warning(
|
|
158
|
+
"Resource has diverged from current loaded data, applying bandaid fix"
|
|
159
|
+
)
|
|
160
|
+
self._current_content_resources[
|
|
161
|
+
resource.id
|
|
162
|
+
].download_state = resource.download_state
|
|
163
|
+
|
|
177
164
|
self._show_page_content(self._content)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
from PySide6.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
|