qcanvas 1.2.0a1__py3-none-any.whl → 1.2.2__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/icons/__init__.py +56 -9
- 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/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 +43 -9
- 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/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 +1891 -629
- qcanvas/icons/{file-downloaded.svg → universal/downloads/downloaded.svg} +1 -1
- 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 +7 -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 +72 -30
- qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +6 -2
- qcanvas/ui/course_viewer/tabs/file_tab/file_tree.py +17 -13
- qcanvas/ui/course_viewer/tabs/file_tab/pages_file_tree.py +15 -9
- qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +7 -4
- qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +6 -2
- qcanvas/ui/course_viewer/tabs/resource_rich_browser.py +36 -52
- qcanvas/ui/course_viewer/tree_widget_data_item.py +22 -0
- qcanvas/ui/main_ui/course_viewer_container.py +11 -10
- 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 +21 -7
- 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 +21 -3
- qcanvas/util/html_cleaner.py +2 -0
- qcanvas/util/layouts.py +5 -2
- qcanvas/util/settings/_mapped_setting.py +6 -1
- qcanvas/util/settings/_ui_settings.py +1 -1
- qcanvas/util/themes/_theme_changer.py +13 -1
- qcanvas/util/ui_tools.py +5 -1
- {qcanvas-1.2.0a1.dist-info → qcanvas-1.2.2.dist-info}/METADATA +13 -7
- qcanvas-1.2.2.dist-info/RECORD +118 -0
- qcanvas/icons/sync.svg +0 -7
- qcanvas-1.2.0a1.dist-info/RECORD +0 -80
- /qcanvas/icons/{logo-transparent-dark.svg → dark/branding/logo_transparent.svg} +0 -0
- /qcanvas/icons/{logo-transparent-light.svg → light/branding/logo_transparent.svg} +0 -0
- /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.2.0a1.dist-info → qcanvas-1.2.2.dist-info}/WHEEL +0 -0
- {qcanvas-1.2.0a1.dist-info → qcanvas-1.2.2.dist-info}/entry_points.txt +0 -0
|
@@ -5,7 +5,9 @@ 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__)
|
|
@@ -45,14 +47,16 @@ class PageTree(ContentTree[db.Course]):
|
|
|
45
47
|
id=module.id, data=module, strings=[module.name]
|
|
46
48
|
)
|
|
47
49
|
module_widget.setFlags(Qt.ItemFlag.ItemIsEnabled)
|
|
50
|
+
module_widget.setIcon(0, icons.tree_items.module)
|
|
48
51
|
|
|
49
52
|
return module_widget
|
|
50
53
|
|
|
51
54
|
def _create_page_widget(
|
|
52
55
|
self, page: db.ModulePage, sync_receipt: SyncReceipt
|
|
53
|
-
) ->
|
|
54
|
-
page_widget =
|
|
56
|
+
) -> TreeWidgetDataItem:
|
|
57
|
+
page_widget = TreeWidgetDataItem(id=page.id, data=page, strings=[page.name])
|
|
55
58
|
page_widget.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable)
|
|
59
|
+
page_widget.setIcon(0, icons.tree_items.page)
|
|
56
60
|
|
|
57
61
|
if sync_receipt.was_updated(page):
|
|
58
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,26 @@ class ResourceRichBrowser(QTextBrowser):
|
|
|
121
102
|
# },
|
|
122
103
|
# )
|
|
123
104
|
# else:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
_logger.debug(str(file_link_tag))
|
|
135
|
-
|
|
136
|
-
return file_link_tag
|
|
137
|
-
|
|
138
|
-
def _file_icon_tag(
|
|
139
|
-
self, document: BeautifulSoup, download_state: db.ResourceDownloadState
|
|
140
|
-
) -> Tag:
|
|
141
|
-
return document.new_tag(
|
|
142
|
-
"img",
|
|
143
|
-
attrs={
|
|
144
|
-
"src": self._download_state_icon(download_state),
|
|
145
|
-
"style": "vertical-align:middle",
|
|
146
|
-
"width": 18,
|
|
147
|
-
},
|
|
148
|
-
)
|
|
105
|
+
|
|
106
|
+
return BeautifulSoup(
|
|
107
|
+
markup=f"""
|
|
108
|
+
<a href="data:{html.escape(resource_id)}" style="font-weight: normal;">
|
|
109
|
+
<img height="18" src="{html.escape(self._download_state_icon(resource.download_state))}"/>
|
|
110
|
+
{html.escape(resource.file_name)}
|
|
111
|
+
</a>
|
|
112
|
+
""",
|
|
113
|
+
features="html.parser",
|
|
114
|
+
).a
|
|
149
115
|
|
|
150
116
|
def _download_state_icon(self, download_state: db.ResourceDownloadState) -> str:
|
|
117
|
+
icon_path = ":icons/universal/downloads"
|
|
151
118
|
match download_state:
|
|
152
119
|
case db.ResourceDownloadState.DOWNLOADED:
|
|
153
|
-
return
|
|
120
|
+
return f"{icon_path}/downloaded.svg"
|
|
154
121
|
case db.ResourceDownloadState.NOT_DOWNLOADED:
|
|
155
|
-
return
|
|
122
|
+
return f"{icon_path}/not_downloaded.svg"
|
|
156
123
|
case db.ResourceDownloadState.FAILED:
|
|
157
|
-
return
|
|
124
|
+
return f"{icon_path}/download_failed.svg"
|
|
158
125
|
case _:
|
|
159
126
|
raise ValueError()
|
|
160
127
|
|
|
@@ -183,4 +150,21 @@ class ResourceRichBrowser(QTextBrowser):
|
|
|
183
150
|
@Slot(db.Resource)
|
|
184
151
|
def _download_updated(self, resource: db.Resource) -> None:
|
|
185
152
|
if self._content is not None and resource.id in self._current_content_resources:
|
|
153
|
+
# BANDAID FIX: In the following situation:
|
|
154
|
+
# - Download is started
|
|
155
|
+
# - Synchronisation is started
|
|
156
|
+
# - Download finishes AFTER the sync
|
|
157
|
+
# --> `resource` is NOT `self._current_content_resources[resource.id]`, because the sync will reload the
|
|
158
|
+
# resource from the DB, but the downloader will still only know about the old resource object.
|
|
159
|
+
# This causes resources not update their download state in the viewer. This line "fixes" that, but does NOT
|
|
160
|
+
# address the root cause. I think reloading the resource from the DB somewhere is the only true fix for this
|
|
161
|
+
|
|
162
|
+
if self._current_content_resources[resource.id] is not resource:
|
|
163
|
+
_logger.warning(
|
|
164
|
+
"Resource has diverged from current loaded data, applying bandaid fix"
|
|
165
|
+
)
|
|
166
|
+
self._current_content_resources[resource.id].download_state = (
|
|
167
|
+
resource.download_state
|
|
168
|
+
)
|
|
169
|
+
|
|
186
170
|
self._show_page_content(self._content)
|
|
@@ -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
|
|
@@ -6,7 +6,6 @@ import qcanvas_backend.database.types as db
|
|
|
6
6
|
from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
|
|
7
7
|
from qcanvas_backend.net.sync.sync_receipt import SyncReceipt, empty_receipt
|
|
8
8
|
from qtpy.QtCore import Qt, Slot
|
|
9
|
-
from qtpy.QtGui import QIcon
|
|
10
9
|
from qtpy.QtWidgets import *
|
|
11
10
|
|
|
12
11
|
from qcanvas import icons
|
|
@@ -16,7 +15,6 @@ from qcanvas.util import themes
|
|
|
16
15
|
_logger = logging.getLogger(__name__)
|
|
17
16
|
|
|
18
17
|
|
|
19
|
-
# todo needs to handle dark mode
|
|
20
18
|
class _PlaceholderLogo(QLabel):
|
|
21
19
|
"""
|
|
22
20
|
Automatically resizing logo icon for when no course is selected
|
|
@@ -24,12 +22,13 @@ class _PlaceholderLogo(QLabel):
|
|
|
24
22
|
|
|
25
23
|
def __init__(self):
|
|
26
24
|
super().__init__()
|
|
27
|
-
self.
|
|
28
|
-
self._dark_icon = QIcon(icons.logo_transparent_dark)
|
|
25
|
+
self._icon = icons.branding.logo_transparent
|
|
29
26
|
self._old_width = -1
|
|
30
27
|
self._old_height = -1
|
|
31
28
|
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
32
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
|
|
33
32
|
themes.theme_changed().connect(self._theme_changed)
|
|
34
33
|
|
|
35
34
|
def resizeEvent(self, event) -> None:
|
|
@@ -47,12 +46,7 @@ class _PlaceholderLogo(QLabel):
|
|
|
47
46
|
if force or (width != self._old_width and height != self._old_height):
|
|
48
47
|
self._old_width = width
|
|
49
48
|
self._old_height = height
|
|
50
|
-
|
|
51
|
-
return
|
|
52
|
-
|
|
53
|
-
icon = self._dark_icon if themes.is_dark_mode() else self._light_icon
|
|
54
|
-
|
|
55
|
-
self.setPixmap(icon.pixmap(width, height))
|
|
49
|
+
self.setPixmap(self._icon.pixmap(width, height))
|
|
56
50
|
|
|
57
51
|
|
|
58
52
|
class CourseViewerContainer(QStackedWidget):
|
|
@@ -61,12 +55,14 @@ class CourseViewerContainer(QStackedWidget):
|
|
|
61
55
|
self._course_viewers: dict[str, CourseViewer] = {}
|
|
62
56
|
self._downloader = downloader
|
|
63
57
|
self._last_course_id: Optional[str] = None
|
|
58
|
+
self._selected_course: Optional[db.Course] = None
|
|
64
59
|
self._last_sync_receipt: SyncReceipt = empty_receipt()
|
|
65
60
|
self._placeholder = _PlaceholderLogo()
|
|
66
61
|
self.addWidget(self._placeholder)
|
|
67
62
|
|
|
68
63
|
def show_blank(self) -> None:
|
|
69
64
|
self._last_course_id = None
|
|
65
|
+
self._selected_course = None
|
|
70
66
|
self.setCurrentWidget(self._placeholder)
|
|
71
67
|
|
|
72
68
|
def load_course(self, course: db.Course) -> None:
|
|
@@ -82,6 +78,7 @@ class CourseViewerContainer(QStackedWidget):
|
|
|
82
78
|
viewer = self._course_viewers[course.id]
|
|
83
79
|
|
|
84
80
|
self.setCurrentWidget(viewer)
|
|
81
|
+
self._selected_course = course
|
|
85
82
|
self._last_course_id = course.id
|
|
86
83
|
|
|
87
84
|
async def reload_all(
|
|
@@ -92,3 +89,7 @@ class CourseViewerContainer(QStackedWidget):
|
|
|
92
89
|
if course.id in self._course_viewers:
|
|
93
90
|
viewer = self._course_viewers[course.id]
|
|
94
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")
|
|
@@ -221,14 +228,21 @@ class QCanvasWindow(QMainWindow):
|
|
|
221
228
|
opening_progress_dialog = QProgressDialog("Opening canvas", None, 0, 0, self)
|
|
222
229
|
opening_progress_dialog.setWindowTitle("Please wait")
|
|
223
230
|
opening_progress_dialog.show()
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
)
|
|
231
|
+
|
|
232
|
+
open_url = QUrl(await self._qcanvas.canvas_client.get_temporary_session_url())
|
|
233
|
+
_logger.info(f"Opening URL {open_url}")
|
|
234
|
+
QDesktopServices.openUrl(open_url)
|
|
227
235
|
opening_progress_dialog.close()
|
|
228
236
|
|
|
229
237
|
@Slot()
|
|
230
238
|
def _open_downloads_folder(self) -> None:
|
|
231
239
|
directory = self._qcanvas.resource_manager.downloads_folder
|
|
240
|
+
|
|
241
|
+
if self._course_viewer_container.selected_course is not None:
|
|
242
|
+
directory /= self._qcanvas.resource_manager.course_folder_name(
|
|
243
|
+
self._course_viewer_container.selected_course
|
|
244
|
+
)
|
|
245
|
+
|
|
232
246
|
directory.mkdir(parents=True, exist_ok=True)
|
|
233
247
|
|
|
234
248
|
QDesktopServices.openUrl(file_url(directory))
|
|
@@ -54,6 +54,7 @@ class TreeMemory:
|
|
|
54
54
|
def set_expanded(self, node_id: str, expanded: bool) -> None:
|
|
55
55
|
contains = node_id in self._state.collapsed_items
|
|
56
56
|
|
|
57
|
+
# fixme when using a slow usb stick, this can momentarily block the event loop.
|
|
57
58
|
if expanded and contains:
|
|
58
59
|
self._state.collapsed_items.remove(node_id)
|
|
59
60
|
self._state.save()
|
|
@@ -17,7 +17,7 @@ class MemoryTreeWidget(QTreeWidget):
|
|
|
17
17
|
parent: Optional[QWidget] = None,
|
|
18
18
|
):
|
|
19
19
|
super().__init__(parent)
|
|
20
|
-
self._id_map: dict[str,
|
|
20
|
+
self._id_map: dict[str, QTreeWidgetItem] = {}
|
|
21
21
|
self._memory = TreeMemory(tree_name)
|
|
22
22
|
self._suppress_expansion_signals = False
|
|
23
23
|
self._suppress_selection_signal = False
|
|
@@ -100,7 +100,7 @@ class MemoryTreeWidget(QTreeWidget):
|
|
|
100
100
|
while len(widget_stack) > 0:
|
|
101
101
|
item = widget_stack.pop()
|
|
102
102
|
|
|
103
|
-
if
|
|
103
|
+
if hasattr(item, "id"):
|
|
104
104
|
if item.id in self._id_map or item.id in map_updates:
|
|
105
105
|
raise ValueError(f"Item with ID {item.id} is already in the tree")
|
|
106
106
|
|
qcanvas/ui/setup/setup_dialog.py
CHANGED
|
@@ -93,7 +93,7 @@ class SetupDialog(QDialog):
|
|
|
93
93
|
self.setWindowTitle("Configure QCanvas")
|
|
94
94
|
self.setMinimumSize(550, 200)
|
|
95
95
|
self.resize(550, 200)
|
|
96
|
-
self.setWindowIcon(QIcon(icons.main_icon))
|
|
96
|
+
self.setWindowIcon(QIcon(icons.branding.main_icon))
|
|
97
97
|
|
|
98
98
|
self._semaphore = Semaphore()
|
|
99
99
|
|
qcanvas/util/file_icons.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import os.path
|
|
2
3
|
|
|
3
4
|
from qtpy.QtCore import QFileInfo, QMimeDatabase
|
|
4
5
|
from qtpy.QtGui import QIcon
|
|
@@ -17,20 +18,37 @@ if runtime.is_running_on_windows:
|
|
|
17
18
|
|
|
18
19
|
else:
|
|
19
20
|
_mime_database = QMimeDatabase()
|
|
20
|
-
|
|
21
|
+
# This must be initialised lazily because the QApplication might not be initialised at this time
|
|
22
|
+
_default_icon: QIcon | None = None
|
|
23
|
+
_icon_for_suffix: dict[str, QIcon] = {}
|
|
21
24
|
|
|
22
25
|
def icon_for_filename(file_name: str) -> QIcon:
|
|
23
26
|
global _default_icon
|
|
24
27
|
|
|
28
|
+
file_suffix = os.path.splitext(file_name)[1]
|
|
29
|
+
|
|
30
|
+
# Check if we already know what icon this file type has
|
|
31
|
+
if file_suffix in _icon_for_suffix:
|
|
32
|
+
return _icon_for_suffix[file_suffix]
|
|
33
|
+
|
|
34
|
+
# Try to find an icon for this file type
|
|
25
35
|
for mime_type in _mime_database.mimeTypesForFileName(file_name):
|
|
26
36
|
icon = QIcon.fromTheme(mime_type.iconName())
|
|
27
37
|
|
|
28
38
|
if not icon.isNull():
|
|
39
|
+
_icon_for_suffix[file_suffix] = icon
|
|
29
40
|
return icon
|
|
30
41
|
|
|
42
|
+
_lazy_init_default_icon()
|
|
43
|
+
|
|
44
|
+
# No icon for this type of file was found, return default icon
|
|
45
|
+
_icon_for_suffix[file_suffix] = _default_icon
|
|
46
|
+
return _default_icon
|
|
47
|
+
|
|
48
|
+
def _lazy_init_default_icon() -> None:
|
|
49
|
+
global _default_icon
|
|
50
|
+
|
|
31
51
|
if _default_icon is None:
|
|
32
52
|
_default_icon = QApplication.style().standardIcon(
|
|
33
53
|
QStyle.StandardPixmap.SP_FileIcon
|
|
34
54
|
)
|
|
35
|
-
|
|
36
|
-
return _default_icon
|
qcanvas/util/html_cleaner.py
CHANGED
|
@@ -16,6 +16,8 @@ def clean_up_html(html: str) -> str:
|
|
|
16
16
|
_remove_tags(doc.find_all(["link", "script"]))
|
|
17
17
|
# Remove font awesome icons (which don't load anyway)
|
|
18
18
|
_remove_tags(doc.find_all(["span"], class_=["dp-icon-content"]))
|
|
19
|
+
# Remove screen reader elements
|
|
20
|
+
_remove_tags(doc.find_all(class_="screenreader-only"))
|
|
19
21
|
|
|
20
22
|
return str(doc)
|
|
21
23
|
|
qcanvas/util/layouts.py
CHANGED
|
@@ -22,11 +22,14 @@ def layout_widget(layout_type: Type[T], *items: QWidget, **kwargs) -> QWidget:
|
|
|
22
22
|
return widget
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
def layout(layout_type: Type[T], *items: QWidget, **kwargs) -> T:
|
|
25
|
+
def layout(layout_type: Type[T], *items: QWidget | QLayout, **kwargs) -> T:
|
|
26
26
|
result_layout: QLayout = layout_type(**kwargs)
|
|
27
27
|
|
|
28
28
|
for item in items:
|
|
29
|
-
|
|
29
|
+
if isinstance(item, QLayout):
|
|
30
|
+
result_layout.addItem(item)
|
|
31
|
+
else:
|
|
32
|
+
result_layout.addWidget(item)
|
|
30
33
|
|
|
31
34
|
return result_layout
|
|
32
35
|
|
|
@@ -53,8 +53,13 @@ class BoolSetting(MappedSetting[bool]):
|
|
|
53
53
|
try:
|
|
54
54
|
# noinspection PyTypeChecker
|
|
55
55
|
value: str = super()._read()
|
|
56
|
+
|
|
57
|
+
if isinstance(value, bool):
|
|
58
|
+
return value
|
|
59
|
+
|
|
56
60
|
return value.lower() == "true"
|
|
57
|
-
except:
|
|
61
|
+
except Exception as e:
|
|
62
|
+
_logger.error("Could not read setting", exc_info=e)
|
|
58
63
|
return self.default
|
|
59
64
|
|
|
60
65
|
# @override
|
|
@@ -20,7 +20,7 @@ class ThemeSetting(MappedSetting):
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class _UISettings:
|
|
23
|
-
settings = QSettings("QCanvasTeam", "
|
|
23
|
+
settings = QSettings("QCanvasTeam", "UI")
|
|
24
24
|
theme: ThemeSetting = ThemeSetting()
|
|
25
25
|
last_geometry: MappedSetting[QByteArray] = MappedSetting()
|
|
26
26
|
last_window_state: MappedSetting[QByteArray] = MappedSetting()
|
|
@@ -2,6 +2,7 @@ import logging
|
|
|
2
2
|
|
|
3
3
|
import qdarktheme
|
|
4
4
|
from qtpy.QtCore import Slot
|
|
5
|
+
from qtpy.QtGui import QIcon
|
|
5
6
|
from qtpy.QtWidgets import QApplication, QStyleFactory
|
|
6
7
|
|
|
7
8
|
from qcanvas.util.themes._colour_scheme_helper import (
|
|
@@ -17,6 +18,10 @@ default_theme = "auto"
|
|
|
17
18
|
_is_dark_mode: bool | None = None
|
|
18
19
|
_selected_theme: SelectedTheme | None = None
|
|
19
20
|
|
|
21
|
+
_universal_path = ":icons/universal"
|
|
22
|
+
_dark_path = ":icons/dark"
|
|
23
|
+
_light_path = ":icons/light"
|
|
24
|
+
|
|
20
25
|
|
|
21
26
|
def apply(theme: str) -> None:
|
|
22
27
|
global _is_dark_mode, _selected_theme
|
|
@@ -45,6 +50,7 @@ def apply(theme: str) -> None:
|
|
|
45
50
|
_selected_theme = SelectedTheme.NATIVE
|
|
46
51
|
|
|
47
52
|
if was_dark_mode != _is_dark_mode:
|
|
53
|
+
_set_fallback_paths()
|
|
48
54
|
theme_changed().emit()
|
|
49
55
|
|
|
50
56
|
|
|
@@ -66,9 +72,15 @@ def _scheme_changed():
|
|
|
66
72
|
if _selected_theme == SelectedTheme.AUTO:
|
|
67
73
|
apply("auto")
|
|
68
74
|
elif _selected_theme == SelectedTheme.NATIVE:
|
|
69
|
-
# noinspection PyTestUnpassedFixture
|
|
70
75
|
_is_dark_mode = is_dark_colour_scheme()
|
|
76
|
+
_set_fallback_paths()
|
|
71
77
|
theme_changed().emit()
|
|
72
78
|
|
|
73
79
|
|
|
80
|
+
def _set_fallback_paths():
|
|
81
|
+
QIcon.setFallbackSearchPaths(
|
|
82
|
+
[_dark_path if _is_dark_mode else _light_path, _universal_path]
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
74
86
|
colour_scheme_changed().connect(_scheme_changed)
|
qcanvas/util/ui_tools.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from typing import Any
|
|
3
3
|
|
|
4
|
-
from qtpy.QtGui import QKeySequence
|
|
4
|
+
from qtpy.QtGui import QIcon, QKeySequence, QPixmap
|
|
5
5
|
from qtpy.QtWidgets import *
|
|
6
6
|
|
|
7
7
|
_logger = logging.getLogger(__name__)
|
|
@@ -19,6 +19,7 @@ def create_qaction(
|
|
|
19
19
|
triggered: Any = None,
|
|
20
20
|
checkable: bool | None = None,
|
|
21
21
|
checked: bool | None = None,
|
|
22
|
+
icon: QIcon | QPixmap | None = None
|
|
22
23
|
) -> QAction:
|
|
23
24
|
action = QAction(name)
|
|
24
25
|
|
|
@@ -38,4 +39,7 @@ def create_qaction(
|
|
|
38
39
|
if checked is not None:
|
|
39
40
|
action.setChecked(checked)
|
|
40
41
|
|
|
42
|
+
if icon is not None:
|
|
43
|
+
action.setIcon(icon)
|
|
44
|
+
|
|
41
45
|
return action
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: qcanvas
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: QCanvas is a desktop client for Canvas LMS.
|
|
5
5
|
Author: QCanvas
|
|
6
6
|
Author-email: QCanvas@noreply.codeberg.org
|
|
7
|
-
Requires-Python:
|
|
7
|
+
Requires-Python: >3.11.0,<3.13
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
10
9
|
Classifier: Programming Language :: Python :: 3.12
|
|
11
10
|
Requires-Dist: aiosqlite (>=0.20.0,<0.21.0)
|
|
12
11
|
Requires-Dist: asynctaskpool (>=0.2.1,<0.3.0)
|
|
@@ -14,11 +13,11 @@ Requires-Dist: lightdb (>=2.0,<3.0)
|
|
|
14
13
|
Requires-Dist: platformdirs (>=4.2.2,<5.0.0)
|
|
15
14
|
Requires-Dist: pyqtdarktheme-fork (>=2.3.2,<3.0.0)
|
|
16
15
|
Requires-Dist: qasync (>=0.27.1,<0.28.0)
|
|
17
|
-
Requires-Dist: qcanvas-api-clients (>=0.
|
|
18
|
-
Requires-Dist: qcanvas-backend (>=0.
|
|
16
|
+
Requires-Dist: qcanvas-api-clients (>=0.4.0,<0.5.0)
|
|
17
|
+
Requires-Dist: qcanvas-backend (>=0.3.0,<0.4.0)
|
|
19
18
|
Requires-Dist: qtpy (>=2.4.1,<3.0.0)
|
|
20
19
|
Requires-Dist: sqlalchemy (>=2.0.31,<3.0.0)
|
|
21
|
-
Requires-Dist: validators (>=0.
|
|
20
|
+
Requires-Dist: validators (>=0.34.0,<0.35.0)
|
|
22
21
|
Description-Content-Type: text/markdown
|
|
23
22
|
|
|
24
23
|
# QCanvas
|
|
@@ -31,7 +30,14 @@ https://github.com/QCanvas/QCanvasApp
|
|
|
31
30
|
|
|
32
31
|
# Downloads
|
|
33
32
|
|
|
34
|
-
|
|
33
|
+
<a href='https://flathub.org/apps/io.github.qcanvas.QCanvasApp'>
|
|
34
|
+
<img width='240' alt='Get it on Flathub' src='https://flathub.org/api/badge?svg&locale=en'/>
|
|
35
|
+
</a>
|
|
36
|
+
|
|
37
|
+
You can download a **windows** version from [releases](https://github.com/QCanvas/QCanvasApp/releases)
|
|
38
|
+
|
|
39
|
+
The appimage version is *not recommended* as it is not a proper portable appimage. It will only work on debian/ubuntu
|
|
40
|
+
based distros.
|
|
35
41
|
|
|
36
42
|
# Development/Run from source
|
|
37
43
|
|