qcanvas 1.1.0__py3-none-any.whl → 1.2.0a0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of qcanvas might be problematic. Click here for more details.
- qcanvas/app_start/__init__.py +6 -1
- qcanvas/icons/__init__.py +1 -0
- qcanvas/icons/file-downloaded.svg +4 -4
- qcanvas/icons/icons.qrc +1 -0
- qcanvas/icons/logo-transparent-light.svg +304 -0
- qcanvas/icons/rc_icons.py +447 -173
- qcanvas/run.py +2 -1
- qcanvas/ui/course_viewer/content_tree.py +13 -0
- qcanvas/ui/course_viewer/course_viewer.py +14 -5
- qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +9 -12
- 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 +95 -0
- qcanvas/ui/course_viewer/tabs/file_tab/pages_file_tree.py +50 -0
- qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +4 -7
- qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +7 -9
- qcanvas/ui/course_viewer/tabs/util.py +10 -0
- qcanvas/ui/main_ui/course_viewer_container.py +32 -2
- qcanvas/ui/main_ui/status_bar_progress_display.py +17 -8
- qcanvas/util/file_icons.py +36 -0
- qcanvas/util/themes.py +7 -1
- {qcanvas-1.1.0.dist-info → qcanvas-1.2.0a0.dist-info}/METADATA +6 -2
- {qcanvas-1.1.0.dist-info → qcanvas-1.2.0a0.dist-info}/RECORD +25 -19
- {qcanvas-1.1.0.dist-info → qcanvas-1.2.0a0.dist-info}/WHEEL +0 -0
- {qcanvas-1.1.0.dist-info → qcanvas-1.2.0a0.dist-info}/entry_points.txt +0 -0
qcanvas/run.py
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
# nuitka-project: --nofollow-import-to=yt_dlp.extractor.lazy_extractors
|
|
24
24
|
|
|
25
25
|
import logging
|
|
26
|
-
from logging import INFO, WARNING
|
|
26
|
+
from logging import DEBUG, INFO, WARNING
|
|
27
27
|
|
|
28
28
|
import qcanvas.app_start
|
|
29
29
|
from qcanvas.util import logs, paths
|
|
@@ -42,6 +42,7 @@ logs.set_levels(
|
|
|
42
42
|
"qcanvas.ui": WARNING,
|
|
43
43
|
"qcanvas_backend": INFO,
|
|
44
44
|
"qcanvas.ui.main_ui.status_bar_progress_display": INFO,
|
|
45
|
+
"qcanvas.util.themes": DEBUG,
|
|
45
46
|
}
|
|
46
47
|
)
|
|
47
48
|
|
|
@@ -61,6 +61,19 @@ class ContentTree(MemoryTreeWidget, Generic[T]):
|
|
|
61
61
|
if min_width is not None:
|
|
62
62
|
self.setMinimumWidth(min_width)
|
|
63
63
|
|
|
64
|
+
def set_columns_resize_mode(
|
|
65
|
+
self,
|
|
66
|
+
resize_mode_for_columns: list[QHeaderView.ResizeMode],
|
|
67
|
+
*,
|
|
68
|
+
stretch_last: bool = False,
|
|
69
|
+
) -> None:
|
|
70
|
+
header = self.header()
|
|
71
|
+
|
|
72
|
+
for index, mode in enumerate(resize_mode_for_columns):
|
|
73
|
+
header.setSectionResizeMode(index, mode)
|
|
74
|
+
|
|
75
|
+
header.setStretchLastSection(stretch_last)
|
|
76
|
+
|
|
64
77
|
def reload(self, data: T, *, sync_receipt: SyncReceipt) -> None:
|
|
65
78
|
self._reloading = True
|
|
66
79
|
|
|
@@ -7,6 +7,7 @@ from qtpy.QtCore import Slot
|
|
|
7
7
|
from qtpy.QtWidgets import *
|
|
8
8
|
|
|
9
9
|
from qcanvas.ui.course_viewer.tabs.assignment_tab import AssignmentTab
|
|
10
|
+
from qcanvas.ui.course_viewer.tabs.file_tab import FileTab
|
|
10
11
|
from qcanvas.ui.course_viewer.tabs.mail_tab import MailTab
|
|
11
12
|
from qcanvas.ui.course_viewer.tabs.page_tab import PageTab
|
|
12
13
|
from qcanvas.util.basic_fonts import bold_font
|
|
@@ -47,12 +48,17 @@ class CourseViewer(QWidget):
|
|
|
47
48
|
downloader=downloader,
|
|
48
49
|
sync_receipt=sync_receipt,
|
|
49
50
|
)
|
|
51
|
+
self._files_tab = FileTab.create_from_receipt(
|
|
52
|
+
course=course,
|
|
53
|
+
downloader=downloader,
|
|
54
|
+
sync_receipt=sync_receipt,
|
|
55
|
+
)
|
|
50
56
|
|
|
51
57
|
self._tabs = QTabWidget()
|
|
58
|
+
self._tabs.addTab(self._files_tab, "Files")
|
|
52
59
|
self._tabs.addTab(self._pages_tab, "Pages")
|
|
53
60
|
self._tabs.addTab(self._assignments_tab, "Assignments")
|
|
54
61
|
self._tabs.addTab(self._mail_tab, "Mail")
|
|
55
|
-
self._tabs.addTab(QLabel("Not implemented"), "Files")
|
|
56
62
|
# self._tabs.addTab(QLabel("Not implemented"), "Panopto") # The meme lives on!
|
|
57
63
|
|
|
58
64
|
self.setLayout(layout(QVBoxLayout, self._course_label, self._tabs))
|
|
@@ -63,10 +69,10 @@ class CourseViewer(QWidget):
|
|
|
63
69
|
self._unhighlight_tab(0) # Because the first tab always gets auto-selected
|
|
64
70
|
|
|
65
71
|
def reload(self, course: db.Course, *, sync_receipt: SyncReceipt) -> None:
|
|
72
|
+
self._files_tab.reload(course, sync_receipt=sync_receipt)
|
|
66
73
|
self._pages_tab.reload(course, sync_receipt=sync_receipt)
|
|
67
74
|
self._assignments_tab.reload(course, sync_receipt=sync_receipt)
|
|
68
75
|
self._mail_tab.reload(course, sync_receipt=sync_receipt)
|
|
69
|
-
|
|
70
76
|
self._highlight_tabs(sync_receipt)
|
|
71
77
|
|
|
72
78
|
@Slot(int)
|
|
@@ -78,14 +84,17 @@ class CourseViewer(QWidget):
|
|
|
78
84
|
updates = sync_receipt.updates_by_course.get(self._course_id, None)
|
|
79
85
|
|
|
80
86
|
if updates is not None:
|
|
81
|
-
if len(updates.
|
|
87
|
+
if len(updates.updated_resources) > 0:
|
|
82
88
|
self._highlight_tab(0)
|
|
83
89
|
|
|
84
|
-
if len(updates.
|
|
90
|
+
if len(updates.updated_pages) > 0:
|
|
85
91
|
self._highlight_tab(1)
|
|
86
92
|
|
|
87
|
-
if len(updates.
|
|
93
|
+
if len(updates.updated_assignments) > 0:
|
|
88
94
|
self._highlight_tab(2)
|
|
95
|
+
|
|
96
|
+
if len(updates.updated_messages) > 0:
|
|
97
|
+
self._highlight_tab(3)
|
|
89
98
|
else:
|
|
90
99
|
for index in range(0, 4):
|
|
91
100
|
self._unhighlight_tab(index)
|
|
@@ -18,6 +18,7 @@ class AssignmentTree(ContentTree[db.Course]):
|
|
|
18
18
|
tree_name=f"course.{course_id}.assignment_groups",
|
|
19
19
|
emit_selection_signal_for_type=db.Assignment,
|
|
20
20
|
)
|
|
21
|
+
|
|
21
22
|
self.ui_setup(
|
|
22
23
|
header_text=["Assignments", "Weight"],
|
|
23
24
|
indentation=15,
|
|
@@ -25,13 +26,9 @@ class AssignmentTree(ContentTree[db.Course]):
|
|
|
25
26
|
min_width=150,
|
|
26
27
|
)
|
|
27
28
|
|
|
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)
|
|
29
|
+
self.set_columns_resize_mode(
|
|
30
|
+
[QHeaderView.ResizeMode.Stretch, QHeaderView.ResizeMode.ResizeToContents]
|
|
31
|
+
)
|
|
35
32
|
|
|
36
33
|
def create_tree_items(
|
|
37
34
|
self, course: db.Course, sync_receipt: SyncReceipt
|
|
@@ -39,8 +36,11 @@ class AssignmentTree(ContentTree[db.Course]):
|
|
|
39
36
|
widgets = []
|
|
40
37
|
|
|
41
38
|
for assignment_group in course.assignment_groups: # type: db.AssignmentGroup
|
|
39
|
+
if len(assignment_group.assignments) == 0:
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
42
|
assignment_group_widget = self._create_assignment_group_widget(
|
|
43
|
-
assignment_group
|
|
43
|
+
assignment_group
|
|
44
44
|
)
|
|
45
45
|
|
|
46
46
|
widgets.append(assignment_group_widget)
|
|
@@ -54,7 +54,7 @@ class AssignmentTree(ContentTree[db.Course]):
|
|
|
54
54
|
return widgets
|
|
55
55
|
|
|
56
56
|
def _create_assignment_group_widget(
|
|
57
|
-
self, assignment_group: db.AssignmentGroup
|
|
57
|
+
self, assignment_group: db.AssignmentGroup
|
|
58
58
|
) -> MemoryTreeWidgetItem:
|
|
59
59
|
assignment_group_widget = MemoryTreeWidgetItem(
|
|
60
60
|
id=assignment_group.id,
|
|
@@ -64,9 +64,6 @@ class AssignmentTree(ContentTree[db.Course]):
|
|
|
64
64
|
|
|
65
65
|
assignment_group_widget.setFlags(Qt.ItemFlag.ItemIsEnabled)
|
|
66
66
|
|
|
67
|
-
if sync_receipt.was_updated(assignment_group):
|
|
68
|
-
self.mark_as_unseen(assignment_group_widget)
|
|
69
|
-
|
|
70
67
|
return assignment_group_widget
|
|
71
68
|
|
|
72
69
|
def _create_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,95 @@
|
|
|
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.memory_tree import MemoryTreeWidgetItem
|
|
12
|
+
from qcanvas.util.file_icons import icon_for_filename
|
|
13
|
+
from qcanvas.util.ui_tools import create_qaction
|
|
14
|
+
|
|
15
|
+
_logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FileTree(ContentTree[db.Course]):
|
|
22
|
+
@classmethod
|
|
23
|
+
def create_from_receipt(
|
|
24
|
+
cls,
|
|
25
|
+
course: db.Course,
|
|
26
|
+
*,
|
|
27
|
+
sync_receipt: SyncReceipt,
|
|
28
|
+
resource_manager: ResourceManager,
|
|
29
|
+
) -> "FileTree":
|
|
30
|
+
tree = cls(tree_name=course.id, resource_manager=resource_manager)
|
|
31
|
+
tree.reload(course, sync_receipt=sync_receipt)
|
|
32
|
+
return tree
|
|
33
|
+
|
|
34
|
+
def __init__(self, tree_name: str, *, resource_manager: ResourceManager):
|
|
35
|
+
super().__init__(tree_name, emit_selection_signal_for_type=object)
|
|
36
|
+
self._resource_manager = resource_manager
|
|
37
|
+
|
|
38
|
+
self.ui_setup(header_text=["File", "Date"])
|
|
39
|
+
self.set_columns_resize_mode(
|
|
40
|
+
[QHeaderView.ResizeMode.Stretch, QHeaderView.ResizeMode.ResizeToContents]
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
44
|
+
self.customContextMenuRequested.connect(self._context_menu)
|
|
45
|
+
|
|
46
|
+
def _create_group_widget(
|
|
47
|
+
self, group: db.ContentGroup, sync_receipt: SyncReceipt
|
|
48
|
+
) -> MemoryTreeWidgetItem:
|
|
49
|
+
group_widget = MemoryTreeWidgetItem(
|
|
50
|
+
id=group.id, data=group, strings=[group.name]
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
group_widget.setFlags(Qt.ItemFlag.ItemIsEnabled)
|
|
54
|
+
|
|
55
|
+
if sync_receipt.was_updated(group):
|
|
56
|
+
self.mark_as_unseen(group_widget)
|
|
57
|
+
|
|
58
|
+
return group_widget
|
|
59
|
+
|
|
60
|
+
def _create_resource_widget(
|
|
61
|
+
self, resource: db.Resource, sync_receipt: SyncReceipt
|
|
62
|
+
) -> QTreeWidgetItem:
|
|
63
|
+
# fixme the reesource widget items shouls NOT be a memory widget item because they can't be collapsed, but
|
|
64
|
+
# mostly because the same file can appear in the tree multiple times in different places, which memory tree
|
|
65
|
+
# can NOT deal with!
|
|
66
|
+
item_widget = QTreeWidgetItem(
|
|
67
|
+
[resource.file_name, str(resource.discovery_date.date())],
|
|
68
|
+
)
|
|
69
|
+
item_widget.setIcon(
|
|
70
|
+
0,
|
|
71
|
+
icon_for_filename(resource.file_name),
|
|
72
|
+
)
|
|
73
|
+
item_widget.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable)
|
|
74
|
+
|
|
75
|
+
if sync_receipt.was_updated(resource):
|
|
76
|
+
self.mark_as_unseen(item_widget)
|
|
77
|
+
|
|
78
|
+
return item_widget
|
|
79
|
+
|
|
80
|
+
@Slot(QPoint)
|
|
81
|
+
def _context_menu(self, point: QPoint) -> None:
|
|
82
|
+
item = self.itemAt(point)
|
|
83
|
+
|
|
84
|
+
if item is None or not isinstance(item, MemoryTreeWidgetItem):
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
menu = QMenu()
|
|
88
|
+
create_qaction(
|
|
89
|
+
name="Test",
|
|
90
|
+
parent=menu,
|
|
91
|
+
triggered=lambda: print(f"Clicked {item.extra_data.file_name}"),
|
|
92
|
+
)
|
|
93
|
+
menu.addAction("Another thing")
|
|
94
|
+
|
|
95
|
+
menu.exec(self.mapToGlobal(point))
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
group_widget = self._create_group_widget(group, sync_receipt)
|
|
31
|
+
items_in_group = set()
|
|
32
|
+
|
|
33
|
+
for item in group.content_items:
|
|
34
|
+
for resource in item.resources: # type: db.Resource
|
|
35
|
+
|
|
36
|
+
if resource.id not in items_in_group:
|
|
37
|
+
items_in_group.add(resource.id)
|
|
38
|
+
else:
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
resource_widget = self._create_resource_widget(
|
|
42
|
+
resource, sync_receipt
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
group_widget.addChild(resource_widget)
|
|
46
|
+
|
|
47
|
+
if group_widget.childCount() > 0:
|
|
48
|
+
widgets.append(group_widget)
|
|
49
|
+
|
|
50
|
+
return widgets
|
|
@@ -18,6 +18,7 @@ class MailTree(ContentTree[db.Course]):
|
|
|
18
18
|
tree_name=f"course.{course_id}.mail",
|
|
19
19
|
emit_selection_signal_for_type=db.CourseMessage,
|
|
20
20
|
)
|
|
21
|
+
|
|
21
22
|
self.ui_setup(
|
|
22
23
|
header_text=["Subject", "Sender"],
|
|
23
24
|
max_width=500,
|
|
@@ -25,13 +26,9 @@ class MailTree(ContentTree[db.Course]):
|
|
|
25
26
|
indentation=20,
|
|
26
27
|
)
|
|
27
28
|
|
|
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)
|
|
29
|
+
self.set_columns_resize_mode(
|
|
30
|
+
[QHeaderView.ResizeMode.Stretch, QHeaderView.ResizeMode.ResizeToContents]
|
|
31
|
+
)
|
|
35
32
|
|
|
36
33
|
def create_tree_items(
|
|
37
34
|
self, course: db.Course, sync_receipt: SyncReceipt
|
|
@@ -1,5 +1,5 @@
|
|
|
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
|
|
@@ -17,6 +17,7 @@ class PageTree(ContentTree[db.Course]):
|
|
|
17
17
|
tree_name=f"course.{course_id}.modules",
|
|
18
18
|
emit_selection_signal_for_type=db.ModulePage,
|
|
19
19
|
)
|
|
20
|
+
|
|
20
21
|
self.ui_setup(
|
|
21
22
|
header_text="Content", indentation=15, max_width=300, min_width=150
|
|
22
23
|
)
|
|
@@ -27,7 +28,10 @@ class PageTree(ContentTree[db.Course]):
|
|
|
27
28
|
widgets = []
|
|
28
29
|
|
|
29
30
|
for module in course.modules: # type: db.Module
|
|
30
|
-
|
|
31
|
+
if len(module.pages) == 0:
|
|
32
|
+
continue
|
|
33
|
+
|
|
34
|
+
module_widget = self._create_module_widget(module)
|
|
31
35
|
widgets.append(module_widget)
|
|
32
36
|
|
|
33
37
|
for page in module.pages: # type: db.ModulePage
|
|
@@ -36,18 +40,12 @@ class PageTree(ContentTree[db.Course]):
|
|
|
36
40
|
|
|
37
41
|
return widgets
|
|
38
42
|
|
|
39
|
-
def _create_module_widget(
|
|
40
|
-
self, module: db.Module, sync_receipt: SyncReceipt
|
|
41
|
-
) -> MemoryTreeWidgetItem:
|
|
43
|
+
def _create_module_widget(self, module: db.Module) -> MemoryTreeWidgetItem:
|
|
42
44
|
module_widget = MemoryTreeWidgetItem(
|
|
43
45
|
id=module.id, data=module, strings=[module.name]
|
|
44
46
|
)
|
|
45
47
|
module_widget.setFlags(Qt.ItemFlag.ItemIsEnabled)
|
|
46
48
|
|
|
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
|
-
|
|
51
49
|
return module_widget
|
|
52
50
|
|
|
53
51
|
def _create_page_widget(
|
|
@@ -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: ...
|
|
@@ -1,17 +1,48 @@
|
|
|
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
8
|
from qtpy.QtCore import Qt
|
|
9
|
+
from qtpy.QtGui import QIcon
|
|
8
10
|
from qtpy.QtWidgets import *
|
|
9
11
|
|
|
12
|
+
from qcanvas import icons
|
|
10
13
|
from qcanvas.ui.course_viewer.course_viewer import CourseViewer
|
|
11
14
|
|
|
12
15
|
_logger = logging.getLogger(__name__)
|
|
13
16
|
|
|
14
17
|
|
|
18
|
+
# todo needs to handle dark mode
|
|
19
|
+
class _PlaceholderLogo(QLabel):
|
|
20
|
+
"""
|
|
21
|
+
Automatically resizing logo icon for when no course is selected
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
super().__init__()
|
|
26
|
+
self._icon = QIcon(icons.logo_transparent_light)
|
|
27
|
+
self._old_width = -1
|
|
28
|
+
self._old_height = -1
|
|
29
|
+
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
30
|
+
self.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Ignored)
|
|
31
|
+
|
|
32
|
+
def resizeEvent(self, event) -> None:
|
|
33
|
+
# Calculate the size of the logo as half of the width/height with a max size of 1000x1000
|
|
34
|
+
width = min(floor(self.width() * 0.5), 500)
|
|
35
|
+
height = min(floor(self.height() * 0.5), 500)
|
|
36
|
+
|
|
37
|
+
if width == self._old_width or height == self._old_height:
|
|
38
|
+
return
|
|
39
|
+
else:
|
|
40
|
+
self._old_width = width
|
|
41
|
+
self._old_height = height
|
|
42
|
+
|
|
43
|
+
self.setPixmap(self._icon.pixmap(width, height))
|
|
44
|
+
|
|
45
|
+
|
|
15
46
|
class CourseViewerContainer(QStackedWidget):
|
|
16
47
|
def __init__(self, downloader: ResourceManager):
|
|
17
48
|
super().__init__()
|
|
@@ -19,8 +50,7 @@ class CourseViewerContainer(QStackedWidget):
|
|
|
19
50
|
self._downloader = downloader
|
|
20
51
|
self._last_course_id: Optional[str] = None
|
|
21
52
|
self._last_sync_receipt: SyncReceipt = empty_receipt()
|
|
22
|
-
self._placeholder =
|
|
23
|
-
self._placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
53
|
+
self._placeholder = _PlaceholderLogo()
|
|
24
54
|
self.addWidget(self._placeholder)
|
|
25
55
|
|
|
26
56
|
def show_blank(self) -> None:
|
|
@@ -77,10 +77,13 @@ class StatusBarProgressDisplay(QStatusBar):
|
|
|
77
77
|
async with self._lock:
|
|
78
78
|
if self._has_no_tasks:
|
|
79
79
|
self._show_done()
|
|
80
|
-
elif self._has_single_task:
|
|
81
|
-
self._show_single_task_progress(list(self._tasks.items())[0])
|
|
82
80
|
else:
|
|
83
|
-
|
|
81
|
+
tasks = list(self._tasks.items())
|
|
82
|
+
|
|
83
|
+
if self._has_single_task:
|
|
84
|
+
self._show_single_task_progress(tasks[0])
|
|
85
|
+
else:
|
|
86
|
+
self._show_multiple_tasks_progress(tasks)
|
|
84
87
|
|
|
85
88
|
def _show_done(self) -> None:
|
|
86
89
|
_logger.info("Finished tasks. Tasks: %s", self._tasks)
|
|
@@ -94,18 +97,24 @@ class StatusBarProgressDisplay(QStatusBar):
|
|
|
94
97
|
self._show_progress(progress)
|
|
95
98
|
self.showMessage(id.step_name)
|
|
96
99
|
|
|
97
|
-
def _show_multiple_tasks_progress(
|
|
100
|
+
def _show_multiple_tasks_progress(
|
|
101
|
+
self, tasks: list[Tuple[TaskID, _TaskProgress]]
|
|
102
|
+
) -> None:
|
|
98
103
|
_logger.debug("Multiple tasks %s", tasks)
|
|
99
|
-
self.showMessage(
|
|
104
|
+
self.showMessage(
|
|
105
|
+
f"{len(tasks)} tasks in progress - {', '.join([task[0].step_name for task in tasks])}"
|
|
106
|
+
)
|
|
100
107
|
self._show_progress(self._calculate_progress(tasks))
|
|
101
108
|
|
|
102
|
-
def _calculate_progress(
|
|
103
|
-
|
|
109
|
+
def _calculate_progress(
|
|
110
|
+
self, tasks: list[Tuple[TaskID, _TaskProgress]]
|
|
111
|
+
) -> _TaskProgress:
|
|
112
|
+
# Task progresses are floats from 0 to 1, multiplier is used to turn them into ints
|
|
104
113
|
multiplier = 1000
|
|
105
114
|
current_sum = 0
|
|
106
115
|
total_sum = 0
|
|
107
116
|
|
|
108
|
-
for task in tasks:
|
|
117
|
+
for _, task in tasks:
|
|
109
118
|
if task.total != 0:
|
|
110
119
|
current_sum += (task.current / task.total) * multiplier
|
|
111
120
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from qtpy.QtCore import QFileInfo, QMimeDatabase
|
|
4
|
+
from qtpy.QtGui import QIcon
|
|
5
|
+
from qtpy.QtWidgets import QApplication, QFileIconProvider, QStyle
|
|
6
|
+
|
|
7
|
+
import qcanvas.util.runtime as runtime
|
|
8
|
+
|
|
9
|
+
_logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
# Windows and linux have different ways of doing this
|
|
12
|
+
if runtime.is_running_on_windows:
|
|
13
|
+
_icon_provider = QFileIconProvider()
|
|
14
|
+
|
|
15
|
+
def icon_for_filename(file_name: str) -> QIcon:
|
|
16
|
+
return _icon_provider.icon(QFileInfo(file_name))
|
|
17
|
+
|
|
18
|
+
else:
|
|
19
|
+
_mime_database = QMimeDatabase()
|
|
20
|
+
_default_icon = None
|
|
21
|
+
|
|
22
|
+
def icon_for_filename(file_name: str) -> QIcon:
|
|
23
|
+
global _default_icon
|
|
24
|
+
|
|
25
|
+
for mime_type in _mime_database.mimeTypesForFileName(file_name):
|
|
26
|
+
icon = QIcon.fromTheme(mime_type.iconName())
|
|
27
|
+
|
|
28
|
+
if not icon.isNull():
|
|
29
|
+
return icon
|
|
30
|
+
|
|
31
|
+
if _default_icon is None:
|
|
32
|
+
_default_icon = QApplication.style().standardIcon(
|
|
33
|
+
QStyle.StandardPixmap.SP_FileIcon
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return _default_icon
|
qcanvas/util/themes.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
|
+
import darkdetect
|
|
3
4
|
import qdarktheme
|
|
5
|
+
from qtpy.QtGui import QGuiApplication
|
|
4
6
|
from qtpy.QtWidgets import QApplication, QStyleFactory
|
|
5
7
|
|
|
6
8
|
_logger = logging.getLogger(__name__)
|
|
@@ -19,9 +21,13 @@ def apply(theme: str) -> None:
|
|
|
19
21
|
theme = ensure_theme_is_valid(theme)
|
|
20
22
|
|
|
21
23
|
if theme != "native":
|
|
22
|
-
QApplication.setStyle(QStyleFactory.create("Fusion"))
|
|
23
24
|
|
|
24
25
|
qdarktheme.setup_theme(
|
|
25
26
|
theme,
|
|
26
27
|
custom_colors={"primary": "e02424"},
|
|
27
28
|
)
|
|
29
|
+
|
|
30
|
+
QApplication.setStyle(QStyleFactory.create("Fusion"))
|
|
31
|
+
|
|
32
|
+
_logger.debug("darkdetect says: %s", darkdetect.theme())
|
|
33
|
+
_logger.debug("qt says: %s", QGuiApplication.styleHints().colorScheme())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: qcanvas
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0a0
|
|
4
4
|
Summary: QCanvas is a desktop client for Canvas LMS.
|
|
5
5
|
Author: QCanvas
|
|
6
6
|
Author-email: QCanvas@noreply.codeberg.org
|
|
@@ -15,7 +15,7 @@ Requires-Dist: platformdirs (>=4.2.2,<5.0.0)
|
|
|
15
15
|
Requires-Dist: pyqtdarktheme-fork (>=2.3.2,<3.0.0)
|
|
16
16
|
Requires-Dist: qasync (>=0.27.1,<0.28.0)
|
|
17
17
|
Requires-Dist: qcanvas-api-clients (>=0.3.0,<0.4.0)
|
|
18
|
-
Requires-Dist: qcanvas-backend (>=0.2.
|
|
18
|
+
Requires-Dist: qcanvas-backend (>=0.2.2,<0.3.0)
|
|
19
19
|
Requires-Dist: qtpy (>=2.4.1,<3.0.0)
|
|
20
20
|
Requires-Dist: sqlalchemy (>=2.0.31,<3.0.0)
|
|
21
21
|
Requires-Dist: validators (>=0.33.0,<0.34.0)
|
|
@@ -25,6 +25,10 @@ Description-Content-Type: text/markdown
|
|
|
25
25
|
|
|
26
26
|
QCanvas is an **unofficial** desktop client for Canvas LMS.
|
|
27
27
|
|
|
28
|
+
https://codeberg.org/QCanvas/QCanvas
|
|
29
|
+
|
|
30
|
+
https://github.com/QCanvas/QCanvasApp
|
|
31
|
+
|
|
28
32
|
# Downloads
|
|
29
33
|
|
|
30
34
|
Download it from [releases](https://github.com/QCanvas/QCanvasApp/releases)
|