├── resources ├── canvas.icns ├── canvas.ico └── canvas.png ├── showcase ├── showcase-1.jpg └── showcase-2.jpg ├── .gitignore ├── requirements.txt ├── .github └── workflows │ └── build-sjtu-canvas-app.yml ├── utils.py ├── README.md ├── canvas_api.py └── canvas_downloader.py /resources/canvas.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeTeng2001/SJTU-Canvas-Downloader/HEAD/resources/canvas.icns -------------------------------------------------------------------------------- /resources/canvas.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeTeng2001/SJTU-Canvas-Downloader/HEAD/resources/canvas.ico -------------------------------------------------------------------------------- /resources/canvas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeTeng2001/SJTU-Canvas-Downloader/HEAD/resources/canvas.png -------------------------------------------------------------------------------- /showcase/showcase-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeTeng2001/SJTU-Canvas-Downloader/HEAD/showcase/showcase-1.jpg -------------------------------------------------------------------------------- /showcase/showcase-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeTeng2001/SJTU-Canvas-Downloader/HEAD/showcase/showcase-2.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test_canvas_upload.py 2 | .DS_Store 3 | 4 | # Folder to ignore 5 | .idea 6 | __pycache__ 7 | venv 8 | build 9 | dist 10 | #resources 11 | 12 | *.spec -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | altgraph==0.17.3 2 | arrow==1.2.3 3 | canvasapi==3.0.0 4 | certifi==2022.9.24 5 | charset-normalizer==2.1.1 6 | idna==3.4 7 | macholib==1.16.2 8 | pyinstaller==5.6.2 9 | pyinstaller-hooks-contrib==2022.13 10 | PyQt6==6.4.0 11 | PyQt6-Qt6==6.4.1 12 | PyQt6-sip==13.4.0 13 | python-dateutil==2.8.2 14 | pytz==2022.6 15 | requests==2.28.1 16 | six==1.16.0 17 | urllib3==1.26.13 18 | -------------------------------------------------------------------------------- /.github/workflows/build-sjtu-canvas-app.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Build SJTU Canvas executable 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | build: 15 | runs-on: macos-12 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Set up Python 3.9 19 | uses: actions/setup-python@v3 20 | with: 21 | python-version: "3.9" 22 | cache: 'pip' 23 | - name: Install python dependencies 24 | run: pip install -r requirements.txt 25 | - name: Install macos dependencies 26 | run: brew install create-dmg 27 | - name: Build MacOS application 28 | run : | 29 | pyinstaller -n "SJTU Canvas下载器" -w --icon=resources/canvas.icns canvas_downloader.py 30 | mkdir -p dist/dmg 31 | cp -r dist/SJTU\ Canvas下载器.app dist/dmg 32 | - name: Create dmg 33 | run : 34 | create-dmg 35 | --volname "SJTU Canvas下载器" 36 | --volicon "resources/canvas.icns" 37 | --window-pos 200 120 38 | --window-size 600 300 39 | --icon-size 100 40 | --icon "SJTU Canvas下载器.app" 175 120 41 | --hide-extension "SJTU Canvas下载器.app" 42 | --app-drop-link 425 120 43 | "dist/SJTU Canvas下载器.dmg" 44 | "dist/dmg/" 45 | - name: Upload dmg 46 | uses: actions/upload-artifact@v3.1.2 47 | with: 48 | name: canvas-downloader-macos.dmg 49 | path: "dist/SJTU Canvas下载器.dmg" 50 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtCore import QSettings 2 | from typing import Callable 3 | from pathlib import Path 4 | from enum import Enum 5 | 6 | class ConfigKey(str, Enum): 7 | FOLDER_PATH_ABS = "folder_path_abs" 8 | SECRET_TOKEN = "secret_token" 9 | SYNC_ON = "sync_on" 10 | CANVAS_STRUCT = "canvas_struct" 11 | CLASS_CODE = "class_code" 12 | 13 | DEFAULT_CONFIG_VAL = { 14 | ConfigKey.FOLDER_PATH_ABS: "无", 15 | ConfigKey.SECRET_TOKEN: "无", 16 | ConfigKey.SYNC_ON: True, 17 | ConfigKey.CANVAS_STRUCT: True, 18 | ConfigKey.CLASS_CODE: "0", 19 | } 20 | 21 | def get_application_setting(initialise=False) -> QSettings: 22 | """ 23 | Get the QSettings object to access application configuration 24 | 25 | Args: 26 | initialise: Initialise each missing setting key with a default value 27 | 28 | Returns: 29 | QSettings object 30 | """ 31 | settings = QSettings("sjtu", "lunafreya-canvas-downloader") 32 | 33 | # Set default value for non-existence key 34 | if initialise: 35 | print(f"Config file location:{settings.fileName()}") 36 | for config_key, config_val in DEFAULT_CONFIG_VAL.items(): 37 | if not settings.contains(config_key): 38 | settings.setValue(config_key, config_val) 39 | 40 | return settings 41 | 42 | def check_setting() -> (bool, str): 43 | """ 44 | Check validity of each setting 45 | 46 | Returns: 47 | (True, None) if passed 48 | (False, err_reason) if failed 49 | """ 50 | setting = get_application_setting() 51 | if not Path(setting.value(ConfigKey.FOLDER_PATH_ABS)).is_dir(): 52 | return False, "违法目标路径" 53 | if len(setting.value(ConfigKey.CLASS_CODE)) == 0: 54 | return False, "不能有空的课程号码" 55 | 56 | return True, None 57 | 58 | def print_middle(content: str, print_to: Callable[[str], None], print_length: int = 72): 59 | remaining_length = print_length - len(content.encode('utf-8')) 60 | left_len = int(remaining_length / 2) 61 | right_len = remaining_length - left_len 62 | 63 | print_to(left_len * "-" + content + right_len * "-") 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Canvas下载器演示 2 | 3 | | ![](./showcase/showcase-1.jpg) | ![](./showcase/showcase-2.jpg) | 4 | |--------------------------------|--------------------------------| 5 | 6 | ## 以下是面向用户文档 7 | 8 | ### 下载链接 9 | 1. [Window 下载链接](https://github.com/LeeTeng2001/SJTU-Canvas-Downloader/releases/download/v1.2/SJTU.Canvas.exe) 10 | 2. [Mac 下载链接](https://github.com/LeeTeng2001/SJTU-Canvas-Downloader/releases/download/v1.2/SJTU.Canvas.dmg) 11 | 12 | ### 教程 13 | 1. 首先去[Canvas设置](https://oc.sjtu.edu.cn/profile/settings),滑倒下方,设置**允许外部软件的融入**,并创建访问许可证。访问许可证可以随意填写用途,过期日期可以留空。 14 | 2. 生成后,将Canvas令牌保存在一旁(非常重要) 15 | 3. 找到要下载的课程页面,抄下网址中的**课程号码**。如网址为:https://oc.sjtu.edu.cn/courses/93214 ,那么课程号码就是**93214** 16 | 4. 运行exe程序(应该需要权限),点击**设置**填写Canvas令牌。填写课程号码。 17 | 5. **同步模式** : 推荐使用On模式(只下载新的文件)。程序会先检查目标文件夹下有没有重名的文件(也会检查子文件夹) 18 | 6. **Canvas文件结构**: 是否按照Canvas上的文件夹结构进行下载。 19 | 7. 最后点击保存并运行即可 20 | 8. 注意,Canvas令牌只需要创建一次,以后下载其他课程只需要换课程号码参数即可。 21 | 22 | 23 | ## 以下是面向开发者文档 24 | 25 | ### How to build and run this project locally 26 | 27 | ```bash 28 | # Create new virtual environment and install dependencies 29 | $ python3 -m venv venv 30 | $ source venv/bin/activate 31 | $ pip install -r requirement.txt 32 | 33 | # Run 34 | $ python canvas_downloader.py 35 | 36 | # Build executable and disk (mac) 37 | $ pyinstaller -n "SJTU Canvas下载器" -w --icon=resources/canvas.icns canvas_downloader.py 38 | $ mkdir -p dist/dmg 39 | $ cp -r dist/SJTU\ Canvas下载器.app dist/dmg 40 | $ create-dmg \ 41 | --volname "SJTU Canvas下载器" \ 42 | --volicon "resources/canvas.icns" \ 43 | --window-pos 200 120 \ 44 | --window-size 600 300 \ 45 | --icon-size 100 \ 46 | --icon "SJTU Canvas下载器.app" 175 120 \ 47 | --hide-extension "SJTU Canvas下载器.app" \ 48 | --app-drop-link 425 120 \ 49 | "dist/SJTU Canvas下载器.dmg" \ 50 | "dist/dmg/" 51 | 52 | # Build executable (window) 53 | $ pyinstaller -n "SJTU Canvas下载器" --onefile -w --icon=resources/canvas.ico canvas_downloader.py 54 | ``` 55 | 56 | ### Change Log 57 | - v1.0 58 | - Initial build 59 | - v1.1 60 | - Added sync & ability to choose download structure 61 | - v1.2 (Current release) 62 | - Update to QT6 63 | - restructure project for maintainability 64 | - Add docs and code refactoring 65 | - Remove dependency for `configuration.json` file 66 | - Use pathlib for path manipulation instead of os 67 | - Unit testing 68 | - 更改应用语言为中文 69 | - v1.3 (In Progress) 70 | - Automate build process 71 | -------------------------------------------------------------------------------- /canvas_api.py: -------------------------------------------------------------------------------- 1 | from canvasapi import Canvas, exceptions 2 | from typing import Callable 3 | import os 4 | from pathlib import Path 5 | from utils import get_application_setting, ConfigKey 6 | 7 | app_setting = get_application_setting() 8 | 9 | def download_canvas(print_output: Callable[[str], None]) -> None: 10 | """ 11 | Download files from canvas 12 | 13 | Args: 14 | print_output: output function (eg: terminal or qt output) 15 | """ 16 | canvas_struct = app_setting.value(ConfigKey.CANVAS_STRUCT) 17 | sync_on = app_setting.value(ConfigKey.SYNC_ON) 18 | download_to_folder = app_setting.value(ConfigKey.FOLDER_PATH_ABS) 19 | 20 | try: 21 | canvas = Canvas("https://oc.sjtu.edu.cn/", app_setting.value(ConfigKey.SECRET_TOKEN)) 22 | course = canvas.get_course(app_setting.value(ConfigKey.CLASS_CODE)) 23 | except exceptions.InvalidAccessToken: 24 | print_output("下载失败:请检查你的口令牌") 25 | return 26 | except ValueError: 27 | print_output("下载失败:请确保课程号码已经设置") 28 | return 29 | except exceptions.ResourceDoesNotExist: 30 | print_output("下载失败:课程号码不存在,请检查") 31 | return 32 | except exceptions.CanvasException: 33 | print_output("下载失败:Canvas状态异常") 34 | return 35 | except Exception: 36 | print_output("下载失败:遇到未知错误") 37 | return 38 | 39 | # variables to get all the files in the current save path first for a LOOKUP table 40 | current_files = set() # print this to debug 41 | new_files_idx = 0 42 | for dir_path, dir_names, filenames in os.walk(download_to_folder): 43 | current_files.update(filenames) 44 | 45 | for folder in course.get_folders(): # Get all folders in a course, it contains all sub-folders as well ! 46 | # Extract the top folder path 47 | folder_top = "" if str(folder) == 'course files' else str(folder).split('/', 1)[1] 48 | print_output(f"正在爬取: {folder_top if folder_top else 'Canvas Home'}") 49 | folder_top = "" if not canvas_struct else folder_top # Check download directory structure 50 | 51 | # Download files 52 | for file in folder.get_files(): 53 | if not sync_on or str(file) not in current_files: # Download checking condition 54 | try: 55 | print_output(f"位于{folder_top if folder_top else 'Canvas Home'},正在下载文件:{file}") 56 | 57 | # Create folder to replicate canvas structure 58 | Path(download_to_folder, folder_top).mkdir(parents=True, exist_ok=True) 59 | file.download(Path(app_setting.value(ConfigKey.FOLDER_PATH_ABS), folder_top, str(file)).as_posix()) 60 | new_files_idx += 1 # only increment after download because it might have error during download 61 | except exceptions.ResourceDoesNotExist: 62 | print_output(f"下载失败:资源不存在") 63 | except exceptions.Unauthorized: 64 | print_output(f"下载失败:没有权限") 65 | 66 | # Status report 67 | print_output(f"下载结束,总共下载了{new_files_idx}个文件。") 68 | 69 | 70 | if __name__ == '__main__': 71 | # FOR DEBUGGING 72 | API_KEY = os.getenv("SJTU_CANVAS_KEY") 73 | # COURSE_NUMBER = 28295 74 | # SAVE_FOLDER_LOC = "/Users/lunafreya/Downloads/untitled" 75 | # download_canvas(API_KEY, COURSE_NUMBER, SAVE_FOLDER_LOC, print, True, True) 76 | -------------------------------------------------------------------------------- /canvas_downloader.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtGui import QIntValidator, QIcon 2 | from PyQt6.QtWidgets import QApplication, QMainWindow, QTextEdit, QLabel, QHBoxLayout, QStatusBar, \ 3 | QVBoxLayout, QWidget, QGroupBox, QRadioButton, QPushButton, QButtonGroup, QFileDialog, \ 4 | QLineEdit, QInputDialog, QLayout, QBoxLayout 5 | from PyQt6.QtCore import Qt, pyqtSlot, QThread, pyqtSignal 6 | import sys 7 | from utils import get_application_setting, ConfigKey, print_middle, check_setting 8 | from canvas_api import download_canvas 9 | 10 | # Global value 11 | APP_VERSION = 1.2 12 | app_setting = get_application_setting(initialise=True) 13 | 14 | class Window(QMainWindow): 15 | def __init__(self, parent=None): 16 | # Initialise layouts and widgets 17 | super().__init__(parent) 18 | self._centralWidget = QWidget(self) 19 | self.setCentralWidget(self._centralWidget) 20 | self.generalLayout = QVBoxLayout() 21 | self.generalLayout.setAlignment(Qt.AlignmentFlag.AlignTop) 22 | self._centralWidget.setLayout(self.generalLayout) 23 | 24 | # Basic configuration 25 | self.setWindowTitle(f"Canvas下载器 v{APP_VERSION}") 26 | self.setFixedSize(500, 600) 27 | 28 | # Initialize widgets 29 | self._createConfigForm() 30 | self._createStatus() 31 | self._createConnection() 32 | self._configureThread() 33 | self._outputWelcomeMsg() 34 | 35 | def _createConfigForm(self): 36 | # Add widgets ------------------------------------------------------------------------------ 37 | self.btn_set_secret = QPushButton("设置") 38 | self.secret_token_str = QLabel( 39 | f"Canvas令牌状态: {'存在' if app_setting.value(ConfigKey.SECRET_TOKEN) != '无' else '无'}") 40 | self.btn_select_dest = QPushButton("更改") 41 | self.destination_folder = QLabel( 42 | f"目标路径: {self.get_trimmed_path(app_setting.value(ConfigKey.FOLDER_PATH_ABS))}") 43 | self.btn_save_run = QPushButton("保存并运行") 44 | 45 | # Toggle Buttons ------------------------------------------------------------------------------ 46 | self.sync_group = QButtonGroup() 47 | self.sync_mode = QRadioButton("开启") 48 | sync_mode_off = QRadioButton("关闭") 49 | self.sync_group.addButton(self.sync_mode, 1) 50 | self.sync_group.addButton(sync_mode_off, 2) 51 | 52 | self.canvas_struct_group = QButtonGroup() 53 | self.canvas_struct_on = QRadioButton("开启") 54 | canvas_struct_off = QRadioButton("关闭") 55 | self.canvas_struct_group.addButton(self.canvas_struct_on, 1) 56 | self.canvas_struct_group.addButton(canvas_struct_off, 2) 57 | 58 | self.class_code_input = QLineEdit() 59 | self.class_code_input.setValidator(QIntValidator()) 60 | self.class_code_input.setText(app_setting.value(ConfigKey.CLASS_CODE)) 61 | # self.class_code_input.setMinimumWidth(70) 62 | 63 | self.sync_mode.setChecked(True) if app_setting.value(ConfigKey.SYNC_ON) else sync_mode_off.setChecked(True) 64 | self.canvas_struct_on.setChecked(True) if app_setting.value(ConfigKey.CANVAS_STRUCT) else canvas_struct_off.setChecked( 65 | True) 66 | 67 | # Add to layout ------------------------------------------------------------------------------ 68 | self.formGroupBox = QGroupBox() 69 | self.formGroupBoxLayout = QVBoxLayout() 70 | 71 | self.helper_add_hs([self.btn_set_secret, self.secret_token_str], self.formGroupBoxLayout) 72 | self.helper_add_hs([self.btn_select_dest, self.destination_folder], self.formGroupBoxLayout) 73 | self.helper_add_hs([QLabel("同步模式:"), self.sync_mode, sync_mode_off], self.formGroupBoxLayout) 74 | self.helper_add_hs([QLabel("Canvas文件结构:"), self.canvas_struct_on, canvas_struct_off], 75 | self.formGroupBoxLayout) 76 | self.helper_add_hs([QLabel("课程号码:"), self.class_code_input], self.formGroupBoxLayout) 77 | self.helper_add_hs([self.btn_save_run, QLabel("保持当前设置并运行程序")], self.formGroupBoxLayout) 78 | 79 | self.formGroupBox.setLayout(self.formGroupBoxLayout) 80 | self.generalLayout.addWidget(self.formGroupBox) 81 | 82 | def _configureThread(self): 83 | self.thread = CanvasDownloadThread(self) 84 | self.thread.thread_output.connect(self.thread_print) 85 | self.thread.is_running.connect(self.update_display_canvas_running) 86 | 87 | def _createStatus(self): 88 | self.output = QTextEdit() 89 | self.output.setReadOnly(True) 90 | self.status = QStatusBar() 91 | self.setStatusBar(self.status) 92 | 93 | self.generalLayout.addWidget(self.output) 94 | 95 | def _createConnection(self): 96 | """ 97 | Define button connections with corresponding slot 98 | """ 99 | # self.sync_mode.toggled.connect(lambda x: [btn.setDisabled(not x) for btn in self.method_group.buttons()]) 100 | self.btn_set_secret.clicked.connect(self.enterCanvasSecretToken) 101 | self.btn_select_dest.clicked.connect(self.chooseSaveDstFile) 102 | self.btn_save_run.clicked.connect(self.runGrabCanvas) 103 | # Note: Do not need to create connection for the button group since they're connected! 104 | 105 | def _outputWelcomeMsg(self): 106 | """ 107 | Starting message to print to user 108 | """ 109 | self.output.append(f"感谢你使用Canvas下载器v{APP_VERSION} 💜") 110 | self.output.append("准备就绪!") 111 | print_middle("", self.output.append) 112 | self.status.showMessage("下载器正在等你发布指令喔~") 113 | 114 | def thread_print(self, print_this: str): 115 | """ 116 | Get string output from download thread and print it to user 117 | the reason we do it this way is that it's BETTER to update the GUI in the main thread 118 | 119 | Args: 120 | print_this: content to print to user 121 | """ 122 | self.output.append(print_this) 123 | 124 | def update_display_canvas_running(self, is_running: bool): 125 | """ 126 | Output information to user based on the status of downloading thread 127 | 128 | Args: 129 | is_running: exist running downloading task 130 | """ 131 | if is_running: 132 | self.status.showMessage("下载器正在努力的获得资源。。。") 133 | else: 134 | print_middle("完成✅", self.output.append) 135 | self.status.showMessage("下载器正在休息~") 136 | 137 | @pyqtSlot() 138 | def chooseSaveDstFile(self): 139 | """ 140 | Select a folder as the download destination 141 | """ 142 | file_dialog = QFileDialog() 143 | file_dialog.setFileMode(QFileDialog.FileMode.Directory) 144 | abs_path = file_dialog.getExistingDirectory(self, "选择保存的文件夹") 145 | 146 | if abs_path: # if there's a valid input 147 | shorter_path = self.get_trimmed_path(abs_path) 148 | 149 | self.destination_folder.setText(f"目标路径: {shorter_path}") 150 | self.saveSetting(update_config_code={ConfigKey.FOLDER_PATH_ABS: abs_path}) 151 | self.destination_folder.repaint() 152 | self.output.append(f"目标路径更改为: {abs_path}") 153 | self.output.repaint() 154 | 155 | @pyqtSlot() 156 | def enterCanvasSecretToken(self): 157 | """ 158 | Open a dialog to let the user set their canvas token 159 | """ 160 | token, done = QInputDialog().getText(self, '设置Canvas令牌', '请输入你的Canvas令牌: ', 161 | QLineEdit.EchoMode.Normal, app_setting.value(ConfigKey.SECRET_TOKEN)) 162 | 163 | if done: 164 | self.secret_token_str.setText("Canvas令牌状态: 存在") 165 | self.output.append("已更新Canvas令牌,正在保存设置。。。") 166 | self.saveSetting(update_config_code={ConfigKey.SECRET_TOKEN: token}) 167 | # self.output.append(f"Token: {token}") 168 | else: 169 | self.output.append("取消设置Canvas令牌") 170 | 171 | def saveSetting(self, update_config_code: {} = None): 172 | """ 173 | Crawl and save new application settings 174 | """ 175 | # Default empty update code config 176 | if update_config_code is None: 177 | update_config_code = {} 178 | 179 | # Crawl update value from UI 180 | update_config_ui = { 181 | ConfigKey.SYNC_ON: self.sync_mode.isChecked(), 182 | ConfigKey.CANVAS_STRUCT: self.canvas_struct_on.isChecked(), 183 | ConfigKey.CLASS_CODE: self.class_code_input.text(), 184 | } 185 | 186 | # Save value from UI 187 | for new_config_key, new_config_val in update_config_ui.items(): 188 | app_setting.setValue(new_config_key, new_config_val) 189 | 190 | # Save value from code 191 | for new_config_key, new_config_val in update_config_code.items(): 192 | app_setting.setValue(new_config_key, new_config_val) 193 | 194 | self.output.append("已保存新的设置!") 195 | 196 | @pyqtSlot() 197 | def runGrabCanvas(self): 198 | """ 199 | Check required arguments and run canvas download thread 200 | """ 201 | self.saveSetting() 202 | # Pre check 203 | valid_setting, err_reason = check_setting() 204 | if not valid_setting: 205 | self.output.append(err_reason) 206 | return 207 | 208 | # Trigger download logic in another thread 209 | self.thread.start() 210 | 211 | def closeEvent(self, QCloseEvent): 212 | """ 213 | Save setting when we exit the application 214 | 215 | Args: 216 | QCloseEvent: CloseEvent argument provided by qt 217 | """ 218 | self.saveSetting() 219 | QCloseEvent.accept() 220 | 221 | @staticmethod 222 | def helper_add_hs(widgets: [QWidget], to_layout: QVBoxLayout): 223 | """ 224 | helper function to add a list of left aligned widgets as a horizontal layout to a vertical layout 225 | 226 | Args: 227 | widgets: list of QWidget to turn into horizontal layout 228 | to_layout: the vertical layout to add to 229 | """ 230 | h_layout = QHBoxLayout() 231 | h_layout.setAlignment(Qt.AlignmentFlag.AlignLeft) 232 | widgets[0].setFixedWidth(120) 233 | for widget in widgets: 234 | h_layout.addWidget(widget) 235 | to_layout.addLayout(h_layout) 236 | 237 | @staticmethod 238 | def get_trimmed_path(original_path: str, len_threshold: int = 35) -> str: 239 | """ 240 | Shorten path string for trimmed display 241 | 242 | Args: 243 | original_path: absolute path string 244 | len_threshold: trim to threshold 245 | 246 | Returns: 247 | A trimmed path 248 | """ 249 | while len(original_path) >= len_threshold: 250 | if len(original_path.split("/", 1)) == 1: 251 | break 252 | original_path = "...." + original_path.split("/", 1)[1] 253 | return original_path 254 | 255 | 256 | class CanvasDownloadThread(QThread): 257 | thread_output = pyqtSignal(str) 258 | is_running = pyqtSignal(bool) 259 | 260 | def __init__(self, gui_instance): 261 | super(CanvasDownloadThread, self).__init__() 262 | self.gui_instance = gui_instance 263 | 264 | def run(self): 265 | """ 266 | Run download task in another thread 267 | """ 268 | self.is_running.emit(True) 269 | download_canvas(self.thread_output.emit) 270 | self.is_running.emit(False) 271 | 272 | 273 | if __name__ == '__main__': 274 | # Application entry 275 | app = QApplication(sys.argv) 276 | app.setWindowIcon(QIcon('resources/canvas.ico')) # You can comment this out if you don't have the icon 277 | view = Window() 278 | view.show() 279 | sys.exit(app.exec()) 280 | --------------------------------------------------------------------------------