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
|
@@ -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
|
|
@@ -23,7 +23,7 @@ from qcanvas.ui.main_ui.options.quick_sync_option import QuickSyncOption
|
|
|
23
23
|
from qcanvas.ui.main_ui.options.sync_on_start_option import SyncOnStartOption
|
|
24
24
|
from qcanvas.ui.main_ui.options.theme_selection_menu import ThemeSelectionMenu
|
|
25
25
|
from qcanvas.ui.main_ui.status_bar_progress_display import StatusBarProgressDisplay
|
|
26
|
-
from qcanvas.util import paths, settings
|
|
26
|
+
from qcanvas.util import auto_downloader, paths, settings
|
|
27
27
|
from qcanvas.util.qurl_util import file_url
|
|
28
28
|
from qcanvas.util.ui_tools import create_qaction
|
|
29
29
|
|
|
@@ -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))
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
3
|
import qcanvas.util.settings as settings
|
|
4
|
-
from qcanvas.util import is_url
|
|
4
|
+
from qcanvas.util.url_checker import is_url
|
|
5
5
|
|
|
6
6
|
_logger = logging.getLogger(__name__)
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def needs_setup() -> bool:
|
|
10
|
-
if not is_url(settings.client.panopto_url):
|
|
10
|
+
if not settings.client.panopto_disabled and not is_url(settings.client.panopto_url):
|
|
11
11
|
return True
|
|
12
12
|
elif not is_url(settings.client.canvas_url):
|
|
13
13
|
return True
|
qcanvas/ui/setup/setup_dialog.py
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from threading import Semaphore
|
|
3
|
+
from typing import Optional
|
|
3
4
|
|
|
4
5
|
from qasync import asyncSlot
|
|
5
6
|
from qcanvas_api_clients.canvas import CanvasClient, CanvasClientConfig
|
|
6
7
|
from qcanvas_api_clients.panopto import PanoptoClient, PanoptoClientConfig
|
|
7
8
|
from qcanvas_api_clients.util.request_exceptions import ConfigInvalidError
|
|
8
|
-
from qtpy.QtCore import QUrl, Signal, Slot
|
|
9
|
+
from qtpy.QtCore import Qt, QUrl, Signal, Slot
|
|
9
10
|
from qtpy.QtGui import QDesktopServices, QIcon
|
|
10
11
|
from qtpy.QtWidgets import *
|
|
11
12
|
|
|
12
13
|
import qcanvas.util.settings as settings
|
|
13
14
|
from qcanvas import icons
|
|
14
|
-
from qcanvas.util import
|
|
15
|
-
from qcanvas.util.
|
|
15
|
+
from qcanvas.util.layouts import GridItem, grid_layout_widget, layout
|
|
16
|
+
from qcanvas.util.url_checker import is_url
|
|
16
17
|
|
|
17
18
|
_logger = logging.getLogger(__name__)
|
|
18
19
|
|
|
@@ -21,6 +22,68 @@ _tutorial_url = (
|
|
|
21
22
|
)
|
|
22
23
|
|
|
23
24
|
|
|
25
|
+
class _InputRow:
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
*,
|
|
29
|
+
label: str,
|
|
30
|
+
initial_value: str,
|
|
31
|
+
placeholder_text: Optional[str] = None,
|
|
32
|
+
is_password: bool = False,
|
|
33
|
+
):
|
|
34
|
+
self._label = QLabel(label)
|
|
35
|
+
self._input = QLineEdit(initial_value)
|
|
36
|
+
|
|
37
|
+
if placeholder_text is not None:
|
|
38
|
+
self._input.setPlaceholderText(placeholder_text)
|
|
39
|
+
|
|
40
|
+
if is_password:
|
|
41
|
+
self._input.setEchoMode(QLineEdit.EchoMode.Password)
|
|
42
|
+
|
|
43
|
+
def set_error(self, message: Optional[str]) -> None:
|
|
44
|
+
self._input.setStyleSheet("QLineEdit { border: 1px solid red }")
|
|
45
|
+
self._input.setToolTip(message)
|
|
46
|
+
|
|
47
|
+
def clear_error(self) -> None:
|
|
48
|
+
self._input.setStyleSheet(None)
|
|
49
|
+
self._input.setToolTip(None)
|
|
50
|
+
|
|
51
|
+
def grid_row(self) -> list[QWidget]:
|
|
52
|
+
return [self._label, self._input]
|
|
53
|
+
|
|
54
|
+
def disable(self) -> None:
|
|
55
|
+
self._input.setEnabled(False)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def enabled(self) -> bool:
|
|
59
|
+
return self._input.isEnabled()
|
|
60
|
+
|
|
61
|
+
@enabled.setter
|
|
62
|
+
def enabled(self, value: bool) -> None:
|
|
63
|
+
self._input.setEnabled(value)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def text(self) -> str:
|
|
67
|
+
return self._input.text().strip()
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def url_text(self) -> str:
|
|
71
|
+
url = self.text
|
|
72
|
+
|
|
73
|
+
if not url.startswith("http"):
|
|
74
|
+
return "https://" + url
|
|
75
|
+
else:
|
|
76
|
+
return url
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def is_valid_url(self) -> bool:
|
|
80
|
+
return is_url(self.url_text)
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def is_empty(self) -> bool:
|
|
84
|
+
return len(self.text) == 0
|
|
85
|
+
|
|
86
|
+
|
|
24
87
|
class SetupDialog(QDialog):
|
|
25
88
|
closed = Signal()
|
|
26
89
|
|
|
@@ -30,41 +93,61 @@ class SetupDialog(QDialog):
|
|
|
30
93
|
self.setWindowTitle("Configure QCanvas")
|
|
31
94
|
self.setMinimumSize(550, 200)
|
|
32
95
|
self.resize(550, 200)
|
|
33
|
-
self.setWindowIcon(QIcon(icons.main_icon))
|
|
96
|
+
self.setWindowIcon(QIcon(icons.branding.main_icon))
|
|
34
97
|
|
|
35
98
|
self._semaphore = Semaphore()
|
|
36
|
-
|
|
37
|
-
self._canvas_url_box
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
99
|
+
|
|
100
|
+
self._canvas_url_box = _InputRow(
|
|
101
|
+
label="Canvas URL",
|
|
102
|
+
initial_value=settings.client.canvas_url,
|
|
103
|
+
placeholder_text="https://instance.canvas.com",
|
|
104
|
+
)
|
|
105
|
+
self._panopto_url_box = _InputRow(
|
|
106
|
+
label="Panopto URL",
|
|
107
|
+
initial_value=settings.client.panopto_url,
|
|
108
|
+
placeholder_text="https://instance.panopto.com",
|
|
109
|
+
)
|
|
110
|
+
self._canvas_api_key_box = _InputRow(
|
|
111
|
+
label="Canvas API Key",
|
|
112
|
+
initial_value=settings.client.canvas_api_key,
|
|
113
|
+
is_password=True,
|
|
114
|
+
)
|
|
115
|
+
self._disable_panopto_checkbox = QCheckBox("Continue without Panopto")
|
|
116
|
+
self._disable_panopto_checkbox.checkStateChanged.connect(
|
|
117
|
+
self._disable_panopto_check_changed
|
|
118
|
+
)
|
|
42
119
|
self._button_box = self._setup_button_box()
|
|
43
|
-
self._button_box.accepted.connect(self._accepted)
|
|
44
|
-
self._button_box.helpRequested.connect(self._help_requested)
|
|
45
120
|
self._waiting_indicator = self._setup_progress_bar()
|
|
46
|
-
self._status_bar = QStatusBar()
|
|
47
121
|
|
|
48
122
|
self.setLayout(
|
|
49
123
|
layout(
|
|
50
124
|
QVBoxLayout,
|
|
51
125
|
grid_layout_widget(
|
|
52
126
|
[
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
127
|
+
self._canvas_url_box.grid_row(),
|
|
128
|
+
self._canvas_api_key_box.grid_row(),
|
|
129
|
+
self._panopto_url_box.grid_row(),
|
|
130
|
+
[
|
|
131
|
+
GridItem(
|
|
132
|
+
self._disable_panopto_checkbox,
|
|
133
|
+
col_span=2,
|
|
134
|
+
alignment=Qt.AlignmentFlag.AlignRight,
|
|
135
|
+
)
|
|
136
|
+
],
|
|
56
137
|
]
|
|
57
138
|
),
|
|
58
139
|
self._waiting_indicator,
|
|
59
140
|
self._button_box,
|
|
60
|
-
self._status_bar,
|
|
61
141
|
)
|
|
62
142
|
)
|
|
63
143
|
|
|
64
144
|
def _setup_button_box(self) -> QDialogButtonBox:
|
|
65
145
|
box = QDialogButtonBox()
|
|
66
146
|
box.addButton(QDialogButtonBox.StandardButton.Ok)
|
|
67
|
-
box.addButton("Get a Canvas API
|
|
147
|
+
box.addButton("Get a Canvas API Key", QDialogButtonBox.ButtonRole.HelpRole)
|
|
148
|
+
|
|
149
|
+
box.accepted.connect(self._verify_settings)
|
|
150
|
+
box.helpRequested.connect(self._help_requested)
|
|
68
151
|
return box
|
|
69
152
|
|
|
70
153
|
def _setup_progress_bar(self) -> QProgressBar:
|
|
@@ -81,32 +164,33 @@ class SetupDialog(QDialog):
|
|
|
81
164
|
widget.setSizePolicy(size_policy)
|
|
82
165
|
|
|
83
166
|
@asyncSlot()
|
|
84
|
-
async def
|
|
167
|
+
async def _verify_settings(self) -> None:
|
|
85
168
|
if self._semaphore.acquire(False):
|
|
86
169
|
try:
|
|
87
170
|
self._clear_errors()
|
|
88
171
|
|
|
89
|
-
if not self.
|
|
90
|
-
self._status_bar.showMessage("Invalid input!", 5000)
|
|
172
|
+
if not self._check_all_inputs():
|
|
91
173
|
return
|
|
92
174
|
|
|
93
175
|
self._waiting_indicator.setVisible(True)
|
|
94
|
-
self._status_bar.showMessage("Checking configuration...")
|
|
95
176
|
|
|
96
177
|
canvas_config = CanvasClientConfig(
|
|
97
|
-
api_token=self._canvas_api_key_box.text
|
|
98
|
-
canvas_url=self.
|
|
178
|
+
api_token=self._canvas_api_key_box.text,
|
|
179
|
+
canvas_url=self._canvas_url_box.url_text,
|
|
99
180
|
)
|
|
100
181
|
|
|
101
182
|
if not await self._check_canvas_config(canvas_config):
|
|
102
183
|
return
|
|
103
184
|
|
|
104
|
-
if
|
|
105
|
-
self.
|
|
106
|
-
|
|
185
|
+
if self._panopto_enabled:
|
|
186
|
+
if not await self._check_panopto_config(canvas_config):
|
|
187
|
+
self._show_panopto_help()
|
|
188
|
+
return
|
|
107
189
|
except Exception as e:
|
|
108
|
-
self._status_bar.showMessage(f"An error occurred: {e}", 5000)
|
|
109
190
|
_logger.warning("Checking config failed", exc_info=e)
|
|
191
|
+
|
|
192
|
+
error_box = QErrorMessage(self)
|
|
193
|
+
error_box.showMessage(f"Checking config failed: {e}")
|
|
110
194
|
finally:
|
|
111
195
|
self._waiting_indicator.setVisible(False)
|
|
112
196
|
self._semaphore.release()
|
|
@@ -117,59 +201,40 @@ class SetupDialog(QDialog):
|
|
|
117
201
|
_logger.debug("Validation already in progress")
|
|
118
202
|
|
|
119
203
|
def _clear_errors(self) -> None:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
self._status_bar.clearMessage()
|
|
126
|
-
line_edit.setStyleSheet(None)
|
|
127
|
-
line_edit.setToolTip(None)
|
|
128
|
-
|
|
129
|
-
def _all_inputs_valid(self) -> bool:
|
|
204
|
+
self._canvas_url_box.clear_error()
|
|
205
|
+
self._panopto_url_box.clear_error()
|
|
206
|
+
self._canvas_api_key_box.clear_error()
|
|
207
|
+
|
|
208
|
+
def _check_all_inputs(self) -> bool:
|
|
130
209
|
all_valid = True
|
|
131
210
|
|
|
132
|
-
if not
|
|
211
|
+
if not self._canvas_url_box.is_valid_url:
|
|
133
212
|
all_valid = False
|
|
134
|
-
self.
|
|
135
|
-
|
|
213
|
+
self._canvas_url_box.set_error("Canvas URL is invalid")
|
|
214
|
+
|
|
215
|
+
if self._canvas_api_key_box.is_empty:
|
|
136
216
|
all_valid = False
|
|
137
|
-
self.
|
|
138
|
-
|
|
217
|
+
self._canvas_api_key_box.set_error("Canvas API key is empty")
|
|
218
|
+
|
|
219
|
+
if self._panopto_enabled and not self._panopto_url_box.is_valid_url:
|
|
139
220
|
all_valid = False
|
|
140
|
-
self.
|
|
221
|
+
self._panopto_url_box.set_error("Panopto URL is invalid")
|
|
141
222
|
|
|
142
223
|
return all_valid
|
|
143
224
|
|
|
144
|
-
def _get_url(self, line_edit: QLineEdit) -> str:
|
|
145
|
-
url = line_edit.text().strip()
|
|
146
|
-
|
|
147
|
-
if not url.startswith("http"):
|
|
148
|
-
return "https://" + url
|
|
149
|
-
else:
|
|
150
|
-
return url
|
|
151
|
-
|
|
152
225
|
async def _check_canvas_config(self, canvas_config: CanvasClientConfig) -> bool:
|
|
153
226
|
try:
|
|
154
227
|
await CanvasClient.verify_config(canvas_config)
|
|
155
228
|
return True
|
|
156
229
|
except ConfigInvalidError:
|
|
157
|
-
self.
|
|
230
|
+
self._canvas_api_key_box.set_error("Canvas API key is invalid")
|
|
158
231
|
return False
|
|
159
232
|
|
|
160
|
-
def _show_error(self, line_edit: QLineEdit, text: str) -> None:
|
|
161
|
-
line_edit.setToolTip(text)
|
|
162
|
-
self._waiting_indicator.hide()
|
|
163
|
-
self._highlight_line_edit(line_edit)
|
|
164
|
-
|
|
165
|
-
def _highlight_line_edit(self, line_edit: QLineEdit) -> None:
|
|
166
|
-
line_edit.setStyleSheet("QLineEdit { border: 1px solid red }")
|
|
167
|
-
|
|
168
233
|
async def _check_panopto_config(self, canvas_config: CanvasClientConfig) -> bool:
|
|
169
234
|
client = CanvasClient(canvas_config)
|
|
170
235
|
try:
|
|
171
236
|
await PanoptoClient.verify_config(
|
|
172
|
-
PanoptoClientConfig(panopto_url=self.
|
|
237
|
+
PanoptoClientConfig(panopto_url=self._panopto_url_box.url_text),
|
|
173
238
|
client,
|
|
174
239
|
)
|
|
175
240
|
return True
|
|
@@ -194,15 +259,21 @@ class SetupDialog(QDialog):
|
|
|
194
259
|
|
|
195
260
|
@Slot()
|
|
196
261
|
def _open_panopto_login(self) -> None:
|
|
197
|
-
url = QUrl(self.
|
|
262
|
+
url = QUrl(self._panopto_url_box.url_text)
|
|
198
263
|
url.setPath("/Panopto/Pages/Auth/Login.aspx")
|
|
199
264
|
url.setQuery("instance=Canvas&AllowBounce=true")
|
|
200
265
|
QDesktopServices.openUrl(url)
|
|
201
266
|
|
|
202
267
|
def _save_and_close(self) -> None:
|
|
203
|
-
settings.client.canvas_url = self.
|
|
204
|
-
|
|
205
|
-
|
|
268
|
+
settings.client.canvas_url = self._canvas_url_box.url_text
|
|
269
|
+
|
|
270
|
+
if self._panopto_enabled:
|
|
271
|
+
settings.client.panopto_url = self._panopto_url_box.url_text
|
|
272
|
+
else:
|
|
273
|
+
settings.client.panopto_disabled = True
|
|
274
|
+
|
|
275
|
+
settings.client.canvas_api_key = self._canvas_api_key_box.text
|
|
276
|
+
|
|
206
277
|
self.closed.emit()
|
|
207
278
|
self.close()
|
|
208
279
|
|
|
@@ -223,3 +294,11 @@ class SetupDialog(QDialog):
|
|
|
223
294
|
@Slot()
|
|
224
295
|
def _open_tutorial(self) -> None:
|
|
225
296
|
QDesktopServices.openUrl(QUrl(_tutorial_url))
|
|
297
|
+
|
|
298
|
+
@Slot(Qt.CheckState)
|
|
299
|
+
def _disable_panopto_check_changed(self, state: Qt.CheckState) -> None:
|
|
300
|
+
self._panopto_url_box.enabled = state == Qt.CheckState.Unchecked
|
|
301
|
+
|
|
302
|
+
@property
|
|
303
|
+
def _panopto_enabled(self) -> bool:
|
|
304
|
+
return self._disable_panopto_checkbox.checkState() == Qt.CheckState.Unchecked
|
qcanvas/util/__init__.py
CHANGED
qcanvas/util/auto_downloader.py
CHANGED
|
@@ -4,8 +4,7 @@ import logging
|
|
|
4
4
|
import qcanvas_backend.database.types as db
|
|
5
5
|
from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
|
|
6
6
|
from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
|
|
7
|
-
from qtpy.QtWidgets import QMessageBox
|
|
8
|
-
from qtpy.QtWidgets import QWidget
|
|
7
|
+
from qtpy.QtWidgets import QMessageBox, QWidget
|
|
9
8
|
|
|
10
9
|
from qcanvas.util import settings
|
|
11
10
|
|