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.
- 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 +2164 -349
- qcanvas/icons/{file-downloaded.svg → universal/downloads/downloaded.svg} +4 -4
- 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/ui/course_viewer/content_tree.py +20 -3
- 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 +80 -23
- 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 +44 -52
- 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 +17 -4
- 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_dialog.py +1 -1
- qcanvas/util/file_icons.py +54 -0
- qcanvas/util/html_cleaner.py +2 -0
- qcanvas/util/layouts.py +5 -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.1.0.dist-info → qcanvas-1.2.0.dist-info}/METADATA +15 -5
- qcanvas-1.2.0.dist-info/RECORD +118 -0
- qcanvas/icons/sync.svg +0 -7
- qcanvas/util/themes.py +0 -27
- qcanvas-1.1.0.dist-info/RECORD +0 -69
- /qcanvas/icons/{main_icon.svg → universal/branding/main_icon.svg} +0 -0
- /qcanvas/icons/{file-download-failed.svg → universal/downloads/download_failed.svg} +0 -0
- /qcanvas/icons/{file-not-downloaded.svg → universal/downloads/not_downloaded.svg} +0 -0
- /qcanvas/icons/{file-unknown.svg → universal/downloads/unknown.svg} +0 -0
- {qcanvas-1.1.0.dist-info → qcanvas-1.2.0.dist-info}/WHEEL +0 -0
- {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.
|
|
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__()
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
128
|
+
return f"{icon_path}/downloaded.svg"
|
|
154
129
|
case db.ResourceDownloadState.NOT_DOWNLOADED:
|
|
155
|
-
return
|
|
130
|
+
return f"{icon_path}/not_downloaded.svg"
|
|
156
131
|
case db.ResourceDownloadState.FAILED:
|
|
157
|
-
return
|
|
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 =
|
|
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__("
|
|
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,
|
|
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(
|
|
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="
|
|
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",
|
|
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))
|