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.
- qcanvas/app_start/__init__.py +6 -1
- qcanvas/icons/__init__.py +55 -6
- qcanvas/icons/_icon_type.py +42 -0
- qcanvas/icons/_update_icons.py +89 -0
- qcanvas/icons/dark/actions/exit.svg +3 -0
- qcanvas/icons/dark/actions/mark_all_read.svg +3 -0
- qcanvas/icons/dark/actions/open_downloads.svg +3 -0
- qcanvas/icons/dark/actions/quick_login.svg +3 -0
- qcanvas/icons/dark/actions/sync.svg +3 -0
- qcanvas/icons/dark/branding/logo_transparent.svg +303 -0
- qcanvas/icons/dark/options/auto_download.svg +3 -0
- qcanvas/icons/dark/options/theme.svg +3 -0
- qcanvas/icons/dark/tabs/assignments.svg +3 -0
- qcanvas/icons/dark/tabs/mail.svg +3 -0
- qcanvas/icons/dark/tabs/pages.svg +3 -0
- qcanvas/icons/dark/tree_items/assignment.svg +3 -0
- qcanvas/icons/dark/tree_items/mail.svg +3 -0
- qcanvas/icons/dark/tree_items/module.svg +3 -0
- qcanvas/icons/dark/tree_items/page.svg +3 -0
- qcanvas/icons/icons.qrc +44 -8
- qcanvas/icons/light/actions/exit.svg +3 -0
- qcanvas/icons/light/actions/mark_all_read.svg +3 -0
- qcanvas/icons/light/actions/open_downloads.svg +3 -0
- qcanvas/icons/light/actions/quick_login.svg +3 -0
- qcanvas/icons/light/actions/sync.svg +3 -0
- qcanvas/icons/light/branding/logo_transparent.svg +304 -0
- qcanvas/icons/light/options/auto_download.svg +3 -0
- qcanvas/icons/light/options/ignore_old.svg +3 -0
- qcanvas/icons/light/options/include_videos.svg +3 -0
- qcanvas/icons/light/options/theme.svg +3 -0
- qcanvas/icons/light/tabs/assignments.svg +3 -0
- qcanvas/icons/light/tabs/mail.svg +3 -0
- qcanvas/icons/light/tabs/pages.svg +3 -0
- qcanvas/icons/light/tree_items/assignment.svg +3 -0
- qcanvas/icons/light/tree_items/mail.svg +3 -0
- qcanvas/icons/light/tree_items/module.svg +3 -0
- qcanvas/icons/light/tree_items/page.svg +3 -0
- qcanvas/icons/rc_icons.py +2165 -355
- qcanvas/icons/universal/downloads/download_failed.svg +23 -0
- qcanvas/icons/universal/downloads/downloaded.svg +23 -0
- qcanvas/icons/universal/downloads/not_downloaded.svg +23 -0
- qcanvas/icons/universal/tabs/assignments_new_content.svg +3 -0
- qcanvas/icons/universal/tabs/mail_new_content.svg +3 -0
- qcanvas/icons/universal/tabs/pages_new_content.svg +3 -0
- qcanvas/icons/universal/tree_items/semester.svg +108 -0
- qcanvas/run.py +24 -0
- qcanvas/ui/course_viewer/content_tree.py +28 -7
- 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} +20 -6
- qcanvas/ui/course_viewer/course_viewer.py +71 -24
- qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +15 -14
- 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 +99 -0
- qcanvas/ui/course_viewer/tabs/file_tab/pages_file_tree.py +56 -0
- qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +11 -11
- qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +13 -11
- qcanvas/ui/course_viewer/tabs/resource_rich_browser.py +57 -56
- qcanvas/ui/course_viewer/tabs/util.py +10 -0
- qcanvas/ui/course_viewer/tree_widget_data_item.py +22 -0
- qcanvas/ui/main_ui/course_viewer_container.py +46 -3
- qcanvas/ui/main_ui/options/auto_download_resources_option.py +3 -1
- qcanvas/ui/main_ui/options/theme_selection_menu.py +2 -0
- qcanvas/ui/main_ui/qcanvas_window.py +18 -5
- qcanvas/ui/main_ui/status_bar_progress_display.py +17 -8
- qcanvas/ui/memory_tree/_tree_memory.py +1 -0
- qcanvas/ui/memory_tree/memory_tree_widget.py +2 -2
- qcanvas/ui/setup/setup_checker.py +2 -2
- qcanvas/ui/setup/setup_dialog.py +145 -66
- qcanvas/util/__init__.py +0 -2
- qcanvas/util/auto_downloader.py +1 -2
- qcanvas/util/file_icons.py +54 -0
- qcanvas/util/html_cleaner.py +2 -0
- qcanvas/util/layouts.py +5 -2
- qcanvas/util/paths.py +15 -26
- qcanvas/util/runtime.py +20 -0
- qcanvas/util/settings/_client_settings.py +11 -2
- qcanvas/util/settings/_mapped_setting.py +6 -1
- qcanvas/util/themes/__init__.py +2 -0
- qcanvas/util/themes/_colour_scheme_helper.py +38 -0
- qcanvas/util/themes/_selected_theme.py +10 -0
- qcanvas/util/themes/_theme_changed_event.py +17 -0
- qcanvas/util/themes/_theme_changer.py +86 -0
- qcanvas/util/ui_tools.py +5 -1
- {qcanvas-1.0.12.dev3.dist-info → qcanvas-1.2.0.dist-info}/METADATA +16 -6
- qcanvas-1.2.0.dist-info/RECORD +118 -0
- 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/sync.svg +0 -7
- qcanvas/util/themes.py +0 -24
- qcanvas-1.0.12.dev3.dist-info/RECORD +0 -68
- /qcanvas/icons/{main_icon.svg → universal/branding/main_icon.svg} +0 -0
- /qcanvas/icons/{file-unknown.svg → universal/downloads/unknown.svg} +0 -0
- {qcanvas-1.0.12.dev3.dist-info → qcanvas-1.2.0.dist-info}/WHEEL +0 -0
- {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.
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
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
|
|
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
|
-
) ->
|
|
75
|
-
assignment_widget =
|
|
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.
|
|
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.
|
|
29
|
-
|
|
30
|
-
|
|
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[
|
|
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
|
-
) ->
|
|
50
|
-
message_widget =
|
|
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
|
|
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
|
-
|
|
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
|
-
) ->
|
|
56
|
-
page_widget =
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
128
|
+
return f"{icon_path}/downloaded.svg"
|
|
145
129
|
case db.ResourceDownloadState.NOT_DOWNLOADED:
|
|
146
|
-
return
|
|
130
|
+
return f"{icon_path}/not_downloaded.svg"
|
|
147
131
|
case db.ResourceDownloadState.FAILED:
|
|
148
|
-
return
|
|
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
|