├── .eslintrc.js ├── .parcelrc ├── LICENSE ├── README.md ├── bulid-win ├── application.spec ├── build.bat ├── cmdtransmitter.exe ├── fav.ico ├── favicon.ico ├── loc.ico ├── nxt.ico ├── pre.ico └── version.rc ├── md-assets ├── setting.jpg ├── wallhaven.jpg └── webui.jpg ├── package.json ├── rdbdb.db ├── requirements.txt └── src ├── py ├── application.py ├── args_definition.py ├── cmdtransmitter.c ├── component.py ├── configurator.py ├── const_config.py ├── controller.py ├── dao.py ├── get_background.py ├── service.py ├── set_background.py ├── utils.py ├── vo.py └── webapp.py └── vue ├── App.vue ├── api └── index.js ├── asset ├── background.jpg ├── bg-dark-grain.png ├── blue-gradients.jpg ├── icon │ ├── iconfont.css │ ├── iconfont.ttf │ ├── iconfont.woff │ └── iconfont.woff2 ├── logo.png └── style.scss ├── component ├── Background.vue ├── Image.vue ├── Tips.js ├── Tips.vue ├── TypeHeader.vue └── Typewriter.vue ├── main.js ├── public ├── favicon.ico └── index.html ├── router └── index.js ├── store ├── getters.js ├── index.js └── modules │ └── app.js ├── util ├── common.js ├── hkmap.js └── request.js └── view ├── 404.vue ├── About.vue ├── Home.vue ├── Setting.vue ├── Test.vue └── Type.vue /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | parser: 'babel-eslint', 5 | sourceType: 'module' 6 | }, 7 | env: { 8 | browser: true, 9 | node: true, 10 | es6: true 11 | }, 12 | extends: ['plugin:vue/recommended', 'eslint:recommended'], 13 | 14 | // add your custom rules here 15 | // it is base on https://github.com/vuejs/eslint-config-vue 16 | rules: { 17 | 'vue/max-attributes-per-line': [2, { 18 | 'singleline': 10, 19 | 'multiline': { 20 | 'max': 1, 21 | 'allowFirstLine': false 22 | } 23 | }], 24 | // 'vue/no-parsing-error': [2, { 25 | // 'x-invalid-end-tag': false, 26 | // 'invalid-first-character-of-tag-name': false 27 | // }], 28 | 'vue/singleline-html-element-content-newline': 'off', 29 | 'vue/multiline-html-element-content-newline': 'off', 30 | 'vue/name-property-casing': ['error', 'PascalCase'], 31 | 'vue/no-v-html': 'off', 32 | 'vue/html-self-closing': ['error', { 33 | 'html': { 34 | 'void': 'never', 35 | 'normal': 'any', 36 | 'component': 'any' 37 | }, 38 | 'svg': 'always', 39 | 'math': 'always' 40 | }], 41 | 'accessor-pairs': 2, 42 | 'arrow-spacing': [2, { 43 | 'before': true, 44 | 'after': true 45 | }], 46 | 'block-spacing': [2, 'always'], 47 | 'brace-style': [2, '1tbs', { 48 | 'allowSingleLine': true 49 | }], 50 | 'camelcase': [0, { 51 | 'properties': 'always' 52 | }], 53 | 'comma-dangle': [2, 'never'], 54 | 'comma-spacing': [2, { 55 | 'before': false, 56 | 'after': true 57 | }], 58 | 'comma-style': [2, 'last'], 59 | 'constructor-super': 2, 60 | 'curly': [2, 'multi-line'], 61 | 'dot-location': [2, 'property'], 62 | 'eol-last': 2, 63 | 'eqeqeq': ['error', 'always', { 'null': 'ignore' }], 64 | 'generator-star-spacing': [2, { 65 | 'before': true, 66 | 'after': true 67 | }], 68 | 'handle-callback-err': [2, '^(err|error)$'], 69 | 'indent': [2, 2, { 70 | 'SwitchCase': 1 71 | }], 72 | 'jsx-quotes': [2, 'prefer-single'], 73 | 'key-spacing': [2, { 74 | 'beforeColon': false, 75 | 'afterColon': true 76 | }], 77 | 'keyword-spacing': [2, { 78 | 'before': true, 79 | 'after': true 80 | }], 81 | 'new-cap': [2, { 82 | 'newIsCap': true, 83 | 'capIsNew': false 84 | }], 85 | 'new-parens': 2, 86 | 'no-array-constructor': 2, 87 | 'no-caller': 2, 88 | 'no-console': 'off', 89 | 'no-class-assign': 2, 90 | 'no-cond-assign': 2, 91 | 'no-const-assign': 2, 92 | 'no-control-regex': 0, 93 | 'no-delete-var': 2, 94 | 'no-dupe-args': 2, 95 | 'no-dupe-class-members': 2, 96 | 'no-dupe-keys': 2, 97 | 'no-duplicate-case': 2, 98 | 'no-empty-character-class': 2, 99 | 'no-empty-pattern': 2, 100 | 'no-eval': 2, 101 | 'no-ex-assign': 2, 102 | 'no-extend-native': 2, 103 | 'no-extra-bind': 2, 104 | 'no-extra-boolean-cast': 2, 105 | 'no-extra-parens': [2, 'functions'], 106 | 'no-fallthrough': 2, 107 | 'no-floating-decimal': 2, 108 | 'no-func-assign': 2, 109 | 'no-implied-eval': 2, 110 | 'no-inner-declarations': [2, 'functions'], 111 | 'no-invalid-regexp': 2, 112 | 'no-irregular-whitespace': 2, 113 | 'no-iterator': 2, 114 | 'no-label-var': 2, 115 | 'no-labels': [2, { 116 | 'allowLoop': false, 117 | 'allowSwitch': false 118 | }], 119 | 'no-lone-blocks': 2, 120 | 'no-mixed-spaces-and-tabs': 2, 121 | 'no-multi-spaces': 2, 122 | 'no-multi-str': 2, 123 | 'no-multiple-empty-lines': [2, { 124 | 'max': 1 125 | }], 126 | 'no-native-reassign': 2, 127 | 'no-negated-in-lhs': 2, 128 | 'no-new-object': 2, 129 | 'no-new-require': 2, 130 | 'no-new-symbol': 2, 131 | 'no-new-wrappers': 2, 132 | 'no-obj-calls': 2, 133 | 'no-octal': 2, 134 | 'no-octal-escape': 2, 135 | 'no-path-concat': 2, 136 | 'no-proto': 2, 137 | 'no-redeclare': 2, 138 | 'no-regex-spaces': 2, 139 | 'no-return-assign': [2, 'except-parens'], 140 | 'no-self-assign': 2, 141 | 'no-self-compare': 2, 142 | 'no-sequences': 2, 143 | 'no-shadow-restricted-names': 2, 144 | 'no-spaced-func': 2, 145 | 'no-sparse-arrays': 2, 146 | 'no-this-before-super': 2, 147 | 'no-throw-literal': 2, 148 | 'no-trailing-spaces': 2, 149 | 'no-undef': 2, 150 | 'no-undef-init': 2, 151 | 'no-unexpected-multiline': 2, 152 | 'no-unmodified-loop-condition': 2, 153 | 'no-unneeded-ternary': [2, { 154 | 'defaultAssignment': false 155 | }], 156 | 'no-unreachable': 2, 157 | 'no-unsafe-finally': 2, 158 | 'no-unused-vars': [2, { 159 | 'vars': 'all', 160 | 'args': 'none' 161 | }], 162 | 'no-useless-call': 2, 163 | 'no-useless-computed-key': 2, 164 | 'no-useless-constructor': 2, 165 | 'no-useless-escape': 0, 166 | 'no-whitespace-before-property': 2, 167 | 'no-with': 2, 168 | 'one-var': [2, { 169 | 'initialized': 'never' 170 | }], 171 | 'operator-linebreak': [2, 'after', { 172 | 'overrides': { 173 | '?': 'before', 174 | ':': 'before' 175 | } 176 | }], 177 | 'padded-blocks': [2, 'never'], 178 | 'quotes': [2, 'single', { 179 | 'avoidEscape': true, 180 | 'allowTemplateLiterals': true 181 | }], 182 | 'semi': [2, 'never'], 183 | 'semi-spacing': [2, { 184 | 'before': false, 185 | 'after': true 186 | }], 187 | 'space-before-blocks': [2, 'always'], 188 | 'space-before-function-paren': [2, 'never'], 189 | 'space-in-parens': [2, 'never'], 190 | 'space-infix-ops': 2, 191 | 'space-unary-ops': [2, { 192 | 'words': true, 193 | 'nonwords': false 194 | }], 195 | 'spaced-comment': [2, 'always', { 196 | 'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ','] 197 | }], 198 | 'template-curly-spacing': [2, 'never'], 199 | 'use-isnan': 2, 200 | 'valid-typeof': 2, 201 | 'wrap-iife': [2, 'any'], 202 | 'yield-star-spacing': [2, 'both'], 203 | 'yoda': [2, 'never'], 204 | 'prefer-const': 2, 205 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 206 | 'object-curly-spacing': [2, 'always', { 207 | objectsInObjects: false 208 | }], 209 | 'array-bracket-spacing': [2, 'never'] 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /.parcelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@parcel/config-default", 4 | "parcel-config-vue2" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Myles Yang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 随机桌面壁纸 2 | 3 | 随机桌面壁纸 ,v2,`WEBUI`界面版,本项目开源地址:[GitHub](https://github.com/snwjas/RandomDesktopBackground-WEBUI) | [Gitee](https://gitee.com/snwjas/random-desktop-background-webui) 4 | 5 | 前身,v1,`控制台`版开源地址:[GitHub](https://gitee.com/link?target=https%3A%2F%2Fgithub.com%2Fsnwjas%2FRandomDesktopBackground) | [Gitee](https://gitee.com/snwjas/random-desktop-background) 6 | 7 | ## 程序简介 8 | 9 | 使用`Python 3.8`、`Vue 2`编写的自动拉取网络图源并进行自动轮换桌面壁纸的`Windows`应用程序。 10 | 11 | 使用它,可以让你的桌面背景像一盒巧克力一样,你永远不知道下一颗是什么味道,是惊还是喜,只有等你去探索去发现。 12 | 13 | 特性: 14 | 15 | - B/S架构开发,提供WEBUI界面可视化配置操作。 16 | - 在线随机壁纸轮换,默认支持[wallhaven](https://wallhaven.cc/)图源,支持自定义图源。 17 | - 支持部分操作全局热键配置,如切换上/下一张壁纸… 18 | - 更多的等待你去发现… 19 | 20 | ## 环境要求 21 | 22 | - Node 23 | - Python 3.8 24 | - virtualenv:`pip install virtualenv` 25 | 26 | ## 安装使用 27 | 28 | 准备好以上环境,即可运行本项目 29 | 30 | 1、初始化:安装web环境依赖;初始化Python虚拟环境并安装相关依赖包 31 | 32 | ``` bash 33 | npm run Init 34 | ``` 35 | 36 | 37 | 38 | 2、开发环境运行本项目 39 | 40 | 2.1、同时运行python服务端和WEBUI前端 41 | 42 | ``` bash 43 | npm run StartDev 44 | ``` 45 | 46 | 2.2、分开运行python服务端和WEBUI前端 47 | 48 | ```bash 49 | npm run PyDev 50 | ``` 51 | 52 | ```bash 53 | npm run WebDev 54 | ``` 55 | 56 | 57 | 58 | 3、打包为EXE可执行程序 59 | 60 | ```bash 61 | npm run BuildExe 62 | ``` 63 | 64 | ## 其他说明 65 | 66 | - 程序界面服务端默认地址为`127.6.6.6:23333`,程序运行时自动打开,默认界面退出5分钟后自动关闭,也可以访问`127.6.6.6:23333/exit`主动退出界面服务。 67 | - 程序在最后一张壁纸切换完毕后就会重新拉取新的随机壁纸;如果网络不可用,自动重新轮换已下载壁纸。 68 | - 程序在Windows`锁屏状态`下(WIN+L)不会进行切换 69 | 70 | ## 操作界面 71 | 72 | **首页** 73 | 74 | ![webui](md-assets/webui.jpg) 75 | 76 | **[wallhaven](https://wallhaven.cc/)图源选择页面** 77 | 78 | ![wallhaven](md-assets/wallhaven.jpg) 79 | 80 | **部分程序设置页面** 81 | 82 | ![setting](md-assets/setting.jpg) 83 | 84 | ## 总结 85 | 86 | 在决定为这个程序编写一个界面时考虑过几种技术选型,一个是`C/S`,另一个是`B/S`。 87 | 88 | 如果采用 C/S 的话自己有两条路走,一个是采用传统的界面程序编写客户端,如 QT 这一类的,但考虑到自己不熟悉,学习成本过高,故放弃了;另外是基于前端技术的客户端,因为考虑到自己对前端的东西有了解,几乎没有学习成本,而且使用前端的东西可以比较容易写出漂亮的界面,然后就尝试了`pywebview`这个框架来开发,后来发现这些打包后的体积都比较大,然后又放弃了。 89 | 90 | 最后折中一下,还是干起了自己的老本行,使用 B/S 架构开发,查了一下目前流行的 Python WEB 框架,选择了`fastapi`,大概入门就开始写了,一套整下来,这个技术选型确实行的通,正常操作是没问题的,打包EXE后的体积也不算太大。其中遇到比较头疼的是 fastapi 启动是以进程方式启动的,搞进程通信实在麻烦,故而放弃了一些点子,但基本功能是保证的。 91 | 92 | 虽然自己不是搞 Python 的,但是使用 Python 写写简单脚本、小应用程序还是十分便利的。Python 赛高~ 93 | -------------------------------------------------------------------------------- /bulid-win/application.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | import os 4 | 5 | block_cipher = None 6 | 7 | root_path = os.path.abspath(os.path.dirname(os.getcwd())) 8 | src_path = os.path.join(root_path, 'src', 'py') 9 | 10 | 11 | def get_module_path(*filename): 12 | return os.path.join(src_path, *filename) 13 | 14 | 15 | a = Analysis([get_module_path('application.py'), 16 | get_module_path('args_definition.py'), 17 | get_module_path('component.py'), 18 | get_module_path('configurator.py'), 19 | get_module_path('const_config.py'), 20 | get_module_path('controller.py'), 21 | get_module_path('dao.py'), 22 | get_module_path('get_background.py'), 23 | get_module_path('service.py'), 24 | get_module_path('set_background.py'), 25 | get_module_path('utils.py'), 26 | get_module_path('vo.py'), 27 | get_module_path('webapp.py'), 28 | ], 29 | pathex=[], 30 | binaries=[], 31 | datas=[ 32 | ('cmdtransmitter.exe', '.'), 33 | (os.path.join(root_path, 'rdbdb.db'), '.'), 34 | (os.path.join(src_path, 'webui'), 'webui'), 35 | ], 36 | hiddenimports=[], 37 | hookspath=[], 38 | runtime_hooks=[], 39 | excludes=[], 40 | win_no_prefer_redirects=False, 41 | win_private_assemblies=False, 42 | cipher=block_cipher) 43 | 44 | pyz = PYZ(a.pure, a.zipped_data, 45 | cipher=block_cipher) 46 | 47 | exe = EXE(pyz, 48 | a.scripts, 49 | [], 50 | exclude_binaries=True, 51 | name='随机桌面壁纸', 52 | version='version.rc', 53 | debug=False, 54 | bootloader_ignore_signals=False, 55 | strip=False, 56 | resources=[], 57 | upx=True, 58 | console=False, 59 | icon=['favicon.ico','pre.ico','nxt.ico','fav.ico','loc.ico']) 60 | 61 | coll = COLLECT(exe, 62 | a.binaries, 63 | a.zipfiles, 64 | a.datas, 65 | strip=False, 66 | upx=True, 67 | upx_exclude=[], 68 | name='随机桌面壁纸') 69 | -------------------------------------------------------------------------------- /bulid-win/build.bat: -------------------------------------------------------------------------------- 1 | chcp 65001 2 | @echo off 3 | 4 | title Python项目打包.exe 5 | 6 | cls 7 | 8 | @REM 打包依赖 pyinstaller 9 | @REM pip install pyinstaller 10 | 11 | ::gcc -mwindows cmdtransmitter.c -o cmdtransmitter.exe 12 | 13 | if exist build rd /S /Q build 14 | 15 | if exist dist rd /S /Q dist 16 | 17 | ..\venv\Scripts\pyinstaller application.spec --clean -y 18 | 19 | echo.&&echo 打包完成!程序位于当前目录的dist文件夹下。 20 | 21 | echo.&&set /p tips=按0键回车打开该目录... 22 | 23 | if %tips% equ 0 explorer /e, dist 24 | -------------------------------------------------------------------------------- /bulid-win/cmdtransmitter.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snwjas/RandomDesktopBackground-WEBUI/1e34fb38a1088687987a9c2acd0ce5386b70489c/bulid-win/cmdtransmitter.exe -------------------------------------------------------------------------------- /bulid-win/fav.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snwjas/RandomDesktopBackground-WEBUI/1e34fb38a1088687987a9c2acd0ce5386b70489c/bulid-win/fav.ico -------------------------------------------------------------------------------- /bulid-win/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snwjas/RandomDesktopBackground-WEBUI/1e34fb38a1088687987a9c2acd0ce5386b70489c/bulid-win/favicon.ico -------------------------------------------------------------------------------- /bulid-win/loc.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snwjas/RandomDesktopBackground-WEBUI/1e34fb38a1088687987a9c2acd0ce5386b70489c/bulid-win/loc.ico -------------------------------------------------------------------------------- /bulid-win/nxt.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snwjas/RandomDesktopBackground-WEBUI/1e34fb38a1088687987a9c2acd0ce5386b70489c/bulid-win/nxt.ico -------------------------------------------------------------------------------- /bulid-win/pre.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snwjas/RandomDesktopBackground-WEBUI/1e34fb38a1088687987a9c2acd0ce5386b70489c/bulid-win/pre.ico -------------------------------------------------------------------------------- /bulid-win/version.rc: -------------------------------------------------------------------------------- 1 | # UTF-8 2 | # 3 | # For more details about fixed file info 'ffi' see: 4 | # http://msdn.microsoft.com/en-us/library/ms646997.aspx 5 | VSVersionInfo( 6 | ffi=FixedFileInfo( 7 | # filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4) 8 | # Set not needed items to zero 0. 9 | filevers=(2, 2022, 1, 1), 10 | prodvers=(2, 2022, 1, 1), 11 | # Contains a bitmask that specifies the valid bits 'flags'r 12 | mask=0x3f, 13 | # Contains a bitmask that specifies the Boolean attributes of the file. 14 | flags=0x0, 15 | # The operating system for which this file was designed. 16 | # 0x4 - NT and there is no need to change it. 17 | OS=0x40004, 18 | # The general type of file. 19 | # 0x1 - the file is an application. 20 | fileType=0x1, 21 | # The function of the file. 22 | # 0x0 - the function is not defined for this fileType 23 | subtype=0x0, 24 | # Creation date and time stamp. 25 | date=(0, 0) 26 | ), 27 | kids=[ 28 | StringFileInfo([ 29 | StringTable( 30 | u'080404b0', 31 | [ 32 | StringStruct(u'Comments', u'简体中文, 支持Unicode'), 33 | StringStruct(u'CompanyName', u'NONE&PERSONAL'), 34 | StringStruct(u'FileDescription', u'随机桌面壁纸'), 35 | StringStruct(u'FileVersion', u'2.2022.1.1'), 36 | StringStruct(u'LegalCopyright', u'Copyright (C) 2022 Myles Yang'), 37 | StringStruct(u'OriginalFilename', u'随机桌面壁纸.exe'), 38 | StringStruct(u'ProductName', u'随机桌面壁纸'), 39 | StringStruct(u'ProductVersion', u'2.2022.1.1') 40 | ] 41 | ) 42 | ]), 43 | VarFileInfo([VarStruct(u'Translation', [2052, 1200])]) 44 | ] 45 | ) 46 | 47 | -------------------------------------------------------------------------------- /md-assets/setting.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snwjas/RandomDesktopBackground-WEBUI/1e34fb38a1088687987a9c2acd0ce5386b70489c/md-assets/setting.jpg -------------------------------------------------------------------------------- /md-assets/wallhaven.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snwjas/RandomDesktopBackground-WEBUI/1e34fb38a1088687987a9c2acd0ce5386b70489c/md-assets/wallhaven.jpg -------------------------------------------------------------------------------- /md-assets/webui.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snwjas/RandomDesktopBackground-WEBUI/1e34fb38a1088687987a9c2acd0ce5386b70489c/md-assets/webui.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "random_desktop_background_gui", 3 | "version": "2.2022.01.01", 4 | "description": "Random Desktop Background WEBUI Version.", 5 | "license": "MIT", 6 | "keywords": [ 7 | "Background", 8 | "Wallpaper" 9 | ], 10 | "author": { 11 | "name": "Myles Yang", 12 | "email": "myles.yang@foxmail.com", 13 | "url": "https://refrain.xyz" 14 | }, 15 | "scripts": { 16 | "Init": "npm install && pip install virtualenv && virtualenv -p python3 venv && venv\\Scripts\\pip.exe install -r requirements.txt", 17 | "StartDev": "concurrently -n py,vue \"venv\\Scripts\\python.exe src\\py\\application.py --env dev\" \"npm run WebDev\"", 18 | "PyDev": "venv\\Scripts\\python.exe src\\py\\application.py --env dev", 19 | "WebDev": "npm run CleanWebDist && parcel serve src/vue/public/index.html --open --cache-dir build/.dcache --dist-dir build/dist", 20 | "TestProd": "npm run BuildWeb && venv\\Scripts\\python.exe src\\py\\application.py --env prod", 21 | "StartTask": "venv\\Scripts\\python.exe src\\py\\application.py --run console --log both", 22 | "CleanBuild": "if exist build rd /S /Q build", 23 | "CleanWebDist": "if exist build\\dist rd /S /Q build\\dist && if exist build\\.dcache rd /S /Q build\\.dcache", 24 | "CleanWebBuild": "if exist build\\webui rd /S /Q build\\webui && if exist build\\.bcache rd /S /Q build\\.bcache", 25 | "CopyWebToPy": "if exist build\\webui ((if exist src\\py\\webui rd /S /Q src\\py\\webui) && xcopy /E/I/Y build\\webui src\\py\\webui)", 26 | "BuildWeb": "npm run CleanWebBuild && parcel build src/vue/public/index.html --no-source-maps --public-url . --cache-dir build/.bcache --dist-dir build/webui", 27 | "BuildExe": "npm run BuildWeb && npm run CopyWebToPy && cd bulid-win && build.bat" 28 | }, 29 | "dependencies": { 30 | "axios": "^0.21.4", 31 | "element-ui": "^2.15.7", 32 | "vue": "2.6.14", 33 | "vue-router": "^3.5.2", 34 | "vuex": "^3.5.2" 35 | }, 36 | "devDependencies": { 37 | "@parcel/transformer-sass": "^2.0.1", 38 | "parcel": "^2.0.1", 39 | "parcel-config-vue2": "^0.1.3", 40 | "parcel-transformer-vue2": "^0.1.7", 41 | "vue-hot-reload-api": "^2.3.4", 42 | "babel-eslint": "^10.1.0", 43 | "eslint": "^7.32.0", 44 | "eslint-plugin-vue": "^7.17.0", 45 | "sass": "^1.44.0", 46 | "concurrently": "^6.5.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /rdbdb.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snwjas/RandomDesktopBackground-WEBUI/1e34fb38a1088687987a9c2acd0ce5386b70489c/rdbdb.db -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | setuptools == 60.8.2 2 | loguru == 0.5.3 3 | fastapi == 0.73.0 4 | uvicorn == 0.17.4 5 | pywin32 == 303 6 | system-hotkey == 1.0.3 7 | requests == 2.24.0 8 | pyinstaller == 4.8 9 | -------------------------------------------------------------------------------- /src/py/application.py: -------------------------------------------------------------------------------- 1 | # -*-coding:utf-8-*- 2 | 3 | """ 4 | 程序启动入口 5 | @author Myles Yang 6 | """ 7 | 8 | import os 9 | import platform 10 | import sys 11 | 12 | import win32con 13 | 14 | import args_definition as argsdef 15 | import configurator as configr 16 | import const_config as const 17 | import utils 18 | import webapp 19 | from component import CustomLogger, SimpleMmapActuator 20 | from get_background import GetBackgroundTask 21 | from set_background import SetBackgroundTask 22 | from vo import E 23 | 24 | app_fullpath = os.path.abspath(sys.argv[0]) 25 | app_dirname = os.path.dirname(app_fullpath) 26 | 27 | logger = CustomLogger() 28 | log = logger.get_logger() 29 | 30 | 31 | def run_in_the_background(): 32 | # 检测程序是否在运行 33 | arg_dict = argsdef.arg_dict 34 | run_type = arg_dict.get(argsdef.ARG_KEY_RUN) 35 | if run_type and run_type != argsdef.ARG_RUN_TYPE_WEBUI: 36 | if utils.is_process_running(configr.get_bpid(), app_fullpath): 37 | utils.create_dialog("检测到程序在运行中,请勿重复启动!", const.dialog_title, 38 | style=win32con.MB_ICONERROR, interval=5) 39 | return 40 | 41 | log.info('程序启动,当前系统操作系统: {}-{}'.format(platform.platform(), platform.architecture())) 42 | try: 43 | 44 | config = configr.parse_config() 45 | 46 | gtask = GetBackgroundTask(config) 47 | stask = SetBackgroundTask(config, gtask.run) 48 | 49 | gtask.init_task(stask) 50 | 51 | stask.run() 52 | 53 | # 记录运行中程序的PID 54 | configr.record_bpid() 55 | except Exception as e: 56 | exc = E().e(e) 57 | log.error("程序启动错误:{}\n{}".format(exc.message, exc.cause)) 58 | 59 | 60 | def run_on_startup(): 61 | """ 62 | 开机时启动,默认以后台方式运行程序 63 | """ 64 | # 创建快捷方式到用户开机启动目录: shell:startup 65 | from service import create_startup_lnk 66 | create_startup_lnk() 67 | # 后台运行 68 | return run_in_the_background() 69 | 70 | 71 | def check() -> bool: 72 | """ 程序启动检测 73 | :return: True 检查通过 74 | """ 75 | import dao 76 | # 检测配置文件 77 | if not os.path.isfile(os.path.join(app_dirname, const.db_name)): 78 | btn_val = utils.create_dialog("配置文件缺失,是否使用默认配置启动?", const.dialog_title, style=win32con.MB_YESNO) 79 | if btn_val != win32con.IDYES: 80 | os._exit(1) 81 | return False 82 | # 加载默认配置 83 | dao.init_db() 84 | return True 85 | 86 | 87 | def main(): 88 | if not check(): 89 | return 90 | 91 | # 获取启动参数 92 | run_args = argsdef.arg_dict 93 | # 程序没指定运行参数,进入WEBUI 94 | run_type = run_args.get(argsdef.ARG_KEY_RUN) 95 | if not run_type: 96 | webapp.run_app() 97 | else: 98 | # 初始化logger 99 | logger.set_logpath(configr.get_log_abspath()) 100 | # 首先确定运行日志记录方式 101 | log_type = run_args.get(argsdef.ARG_KEY_LOG) 102 | if log_type == argsdef.ARG_LOG_TYPE_FILE: # 文件 103 | logger.use_file_logger() 104 | elif log_type == argsdef.ARG_LOG_TYPE_CONSOLE: # 控制台 105 | logger.use_console_logger() 106 | elif log_type == argsdef.ARG_LOG_TYPE_NONE: # 禁用 107 | logger.user_none() 108 | # 最后确定程序运行方式 109 | if run_type == argsdef.ARG_RUN_TYPE_CONSOLE: # 控制台 110 | pass 111 | elif run_type == argsdef.ARG_RUN_TYPE_BACKGROUND: # 后台 112 | if not log_type: logger.use_file_logger() 113 | run_in_the_background() 114 | elif run_type == argsdef.ARG_RUN_TYPE_POWERBOOT: # 开机自启,后台 115 | if not log_type: logger.use_file_logger() 116 | run_on_startup() 117 | elif run_type == argsdef.ARG_RUN_TYPE_WEBUI: # 启动WEBUI 118 | webapp.run_app() 119 | elif run_type == argsdef.ARG_RUN_TYPE_LNK: # 创建程序快捷方式 120 | lnk_args = run_args.get(argsdef.ARG_KEY_LNK) 121 | if lnk_args or lnk_args == []: 122 | lnk_path = None if lnk_args == [] else lnk_args[0] 123 | args = ' '.join(lnk_args[1:]) 124 | utils.create_shortcut(app_fullpath, lnk_path, args) 125 | elif run_type == argsdef.ARG_RUN_TYPE_CMD: # 执行定义命令 126 | cmd = run_args.get(argsdef.ARG_KEY_CMD) 127 | if cmd in argsdef.CHOICES_ARG_CMD_TYPE: 128 | sma = SimpleMmapActuator() 129 | sma.send_command(cmd) 130 | 131 | 132 | if __name__ == '__main__': 133 | main() 134 | -------------------------------------------------------------------------------- /src/py/args_definition.py: -------------------------------------------------------------------------------- 1 | # -*-coding:utf-8-*- 2 | 3 | """ 4 | 程序启动参数定义、获取与解析 5 | 6 | @author Myles Yang 7 | """ 8 | 9 | import argparse 10 | 11 | import const_config as const 12 | 13 | """ 设置命令参数时的KEY """ 14 | # 程序运行方式 15 | ARG_KEY_RUN = ARG_RUN = '--run' 16 | # 程序日志记录方式 17 | ARG_KEY_LOG = ARG_LOG = '--log' 18 | # 创建程序快捷方式 19 | ARG_KEY_LNK = ARG_LNK = '--lnk' 20 | # 启动环境 21 | ARG_KEY_ENV = ARG_ENV = '--env' 22 | # 运行中可执行指令 23 | ARG_KEY_CMD = ARG_CMD = '--cmd' 24 | 25 | """ --run 命令参数选项 """ 26 | # 控制台启动 27 | ARG_RUN_TYPE_CONSOLE = 'console' 28 | # 控制台后台启动 29 | ARG_RUN_TYPE_BACKGROUND = 'background' 30 | # 开机自启,控制台后台启动 31 | ARG_RUN_TYPE_POWERBOOT = 'powerboot' 32 | # 启动WEBUI 33 | ARG_RUN_TYPE_WEBUI = 'webui' 34 | # 创建快捷方式 35 | ARG_RUN_TYPE_LNK = 'lnk' 36 | # 发送执行命令 37 | ARG_RUN_TYPE_CMD = 'cmd' 38 | # 选择项 39 | CHOICES_ARG_RUN_TYPE = [ARG_RUN_TYPE_CONSOLE, ARG_RUN_TYPE_BACKGROUND, ARG_RUN_TYPE_POWERBOOT, 40 | ARG_RUN_TYPE_WEBUI, ARG_RUN_TYPE_LNK, ARG_RUN_TYPE_CMD] 41 | 42 | """ --log 命令参数选项 """ 43 | # 控制台打印方式记录运行日志 44 | ARG_LOG_TYPE_CONSOLE = 'console' 45 | # 文件方式记录运行日志 46 | ARG_LOG_TYPE_FILE = 'file' 47 | # 文件和控制台打印方式记录运行日志 48 | ARG_LOG_TYPE_BOTH = 'both' 49 | # 禁用日志记录 50 | ARG_LOG_TYPE_NONE = 'none' 51 | # 选择项 52 | CHOICES_ARG_LOG_TYPE = [ARG_LOG_TYPE_CONSOLE, ARG_LOG_TYPE_FILE, ARG_LOG_TYPE_BOTH, ARG_LOG_TYPE_NONE] 53 | 54 | """ --env 命令参数选项 """ 55 | # 生产环境 56 | ARG_ENV_TYPE_PROD = 'prod' 57 | # 开发环境 58 | ARG_ENV_TYPE_DEV = 'dev' 59 | # 选择项 60 | CHOICES_ARG_ENV_TYPE = [ARG_ENV_TYPE_PROD, ARG_ENV_TYPE_DEV] 61 | 62 | """ --cmd 命令参数选项 """ 63 | # 下一张壁纸 64 | ARG_CMD_TYPE_NXT = 'nxt' 65 | # 上一张壁纸 66 | ARG_CMD_TYPE_PRE = 'pre' 67 | # 收藏当前壁纸 68 | ARG_CMD_TYPE_FAV = 'fav' 69 | # 定位当前壁纸 70 | ARG_CMD_TYPE_LOC = 'loc' 71 | # 选择项 72 | CHOICES_ARG_CMD_TYPE = [ARG_CMD_TYPE_NXT, ARG_CMD_TYPE_PRE, ARG_CMD_TYPE_FAV, ARG_CMD_TYPE_LOC] 73 | 74 | """ 75 | 定义命令行输入参数 76 | """ 77 | parser = argparse.ArgumentParser( 78 | prog=const.app_name, 79 | description='{}命令行参数'.format(const.app_name), 80 | ) 81 | 82 | parser.add_argument('-r', ARG_RUN, 83 | help='指定程序的运行方式', 84 | type=str, 85 | choices=CHOICES_ARG_RUN_TYPE, 86 | dest=ARG_KEY_RUN 87 | ) 88 | 89 | parser.add_argument('-l', ARG_LOG, 90 | help='指定运行日志记录方式', 91 | type=str, 92 | choices=CHOICES_ARG_LOG_TYPE, 93 | dest=ARG_KEY_LOG 94 | ) 95 | 96 | parser.add_argument('-e', ARG_ENV, 97 | help='指定程序的运行环境', 98 | type=str, 99 | choices=CHOICES_ARG_ENV_TYPE, 100 | dest=ARG_KEY_ENV, 101 | default=ARG_ENV_TYPE_PROD 102 | ) 103 | 104 | parser.add_argument('-s', ARG_LNK, 105 | help='根据给的路径创建程序的快捷方式,与--run组合使用', 106 | type=str, 107 | nargs='*', 108 | dest=ARG_KEY_LNK 109 | ) 110 | 111 | parser.add_argument('-c', ARG_CMD, 112 | help='运行中可执行指令,与--run组合使用', 113 | type=str, 114 | choices=CHOICES_ARG_CMD_TYPE, 115 | dest=ARG_KEY_CMD 116 | ) 117 | 118 | arg_dict = vars(parser.parse_args()) 119 | -------------------------------------------------------------------------------- /src/py/cmdtransmitter.c: -------------------------------------------------------------------------------- 1 | #include 2 | //#include 3 | 4 | int main(int argc, char **argv) 5 | { 6 | 7 | // 参数argv:[[name,] command] 8 | 9 | if (argc < 2 || argc > 3) 10 | { 11 | return 0; 12 | } 13 | 14 | char *name = (argc == 2 ? "GlobalRandomDesktopBackgroundShareMemory" : argv[1]); 15 | char *command = (argc == 2 ? argv[1] : argv[2]); 16 | 17 | HANDLE fmap = OpenFileMapping(FILE_MAP_ALL_ACCESS, -1, name); 18 | if (fmap == NULL) 19 | { 20 | return 0; 21 | } 22 | 23 | LPVOID fmv = MapViewOfFile(fmap, FILE_MAP_ALL_ACCESS, 0, 0, 0); 24 | if (fmv != NULL) 25 | { 26 | strcpy((char *)fmv, command); 27 | UnmapViewOfFile(fmv); 28 | } 29 | 30 | if (fmap != NULL) 31 | { 32 | CloseHandle(fmap); 33 | } 34 | 35 | return 1; 36 | } 37 | -------------------------------------------------------------------------------- /src/py/component.py: -------------------------------------------------------------------------------- 1 | # -*-coding:utf-8-*- 2 | 3 | """ 4 | 组件 5 | 6 | @author Myles Yang 7 | """ 8 | import mmap 9 | import os 10 | import sys 11 | import typing 12 | from threading import Timer 13 | from typing import Dict 14 | 15 | 16 | class SingletonMetaclass(type): 17 | """ 单例元类 """ 18 | 19 | def __init__(cls, *args, **kwargs): 20 | cls.__instance = None 21 | super(SingletonMetaclass, cls).__init__(*args, **kwargs) 22 | 23 | def __call__(cls, *args, **kwargs): 24 | if cls.__instance is None: 25 | cls.__instance = super(SingletonMetaclass, cls).__call__(*args, **kwargs) 26 | return cls.__instance 27 | 28 | 29 | class CustomLogger(object, metaclass=SingletonMetaclass): 30 | """ 自定义日志记录类 """ 31 | from loguru import logger 32 | from loguru._logger import Logger 33 | logger.opt(lazy=True, colors=True) 34 | # 日志格式 35 | __log_format = '{time:YYYY-MM-DD HH:mm:ss,SSS} | {level}\t | {file}:{function}:{line} | {message}' 36 | # 日志类型定义 37 | LOGGING_DEBUG = 10 38 | LOGGING_INFO = 20 39 | LOGGING_WARNING = 30 40 | LOGGING_ERROR = 40 41 | LOGGING_CRITICAL = 50 42 | LOGURU_SUCCESS = 25 43 | 44 | def __init__(self, log_srcpath: str = ''): 45 | self.__log_srcpath = log_srcpath if log_srcpath else '' 46 | 47 | def __add_file_handler(self): 48 | handler_id = self.logger.add( 49 | sink=os.path.join(self.__log_srcpath, 'run.{time:YYYYMMDD}.log'), 50 | format=self.__log_format, 51 | rotation='1 day', 52 | retention=30, 53 | enqueue=True, 54 | encoding='UTF-8' 55 | ) 56 | return handler_id 57 | 58 | def __add_console_handler(self): 59 | info_handler_id = self.logger.add(sink=sys.stdout, 60 | level=self.LOGGING_INFO, 61 | # fg #097D80 62 | format='' + self.__log_format + '', 63 | colorize=True, 64 | filter=lambda record: record["level"].name == "INFO" 65 | ) 66 | err_handler_id = self.logger.add(sink=sys.stdout, 67 | level=self.LOGGING_ERROR, 68 | # fg #F56C6C 69 | format='' + self.__log_format + '', 70 | colorize=True, 71 | filter=lambda record: record["level"].name == "ERROR" 72 | ) 73 | return info_handler_id, err_handler_id 74 | 75 | def use_file_console_logger(self) -> Logger: 76 | self.logger.remove(handler_id=None) 77 | self.__add_file_handler() 78 | self.__add_console_handler() 79 | return self.logger 80 | 81 | def use_console_logger(self) -> Logger: 82 | self.logger.remove(handler_id=None) 83 | self.__add_console_handler() 84 | return self.logger 85 | 86 | def use_file_logger(self) -> Logger: 87 | self.logger.remove(handler_id=None) 88 | self.__add_file_handler() 89 | return self.logger 90 | 91 | def user_none(self) -> Logger: 92 | self.logger.remove(handler_id=None) 93 | return self.logger 94 | 95 | def set_logpath(self, log_srcpath: str): 96 | log_srcpath = log_srcpath if log_srcpath else '' 97 | self.__log_srcpath = log_srcpath 98 | 99 | def get_logger(self) -> Logger: 100 | return self.logger 101 | 102 | 103 | class SimpleTaskTimer(object): 104 | """ 105 | 简单的循环单任务定时器,非阻塞当前线程 106 | 107 | @author Myles Yang 108 | """ 109 | 110 | def __init__(self): 111 | self.__timer: Timer = None 112 | self.__seconds = 0.0 113 | self.__action = None 114 | self.__args = None 115 | self.__kwargs = None 116 | 117 | def run(self, seconds: float, action, args=None, kwargs=None): 118 | """ 119 | 执行循环定时任务 120 | 121 | :param seconds: 任务执行间隔,单位秒 122 | :param action: 任务函数 123 | :param args: 函数参数 124 | """ 125 | if not callable(action): 126 | raise AttributeError("参数action非法,请传入函数变量") 127 | 128 | if self.is_running(): 129 | return 130 | 131 | self.__action = action 132 | self.__seconds = seconds 133 | self.__args = args if args is not None else [] 134 | self.__kwargs = kwargs if kwargs is not None else {} 135 | 136 | self.__run_action() 137 | 138 | def __run_action(self): 139 | self.__timer = Timer(self.__seconds, self.__hook, self.__args, self.__kwargs) 140 | self.__timer.start() 141 | 142 | def __hook(self, *args, **kwargs): 143 | self.__action(*args, **kwargs) 144 | self.__run_action() 145 | 146 | def is_running(self) -> bool: 147 | """ 148 | 判断任务是否在执行 149 | """ 150 | return self.__timer and self.__timer.is_alive() 151 | 152 | def cancel(self): 153 | """ 154 | 取消循环定时任务 155 | """ 156 | if self.is_running(): 157 | self.__timer.cancel() 158 | self.__timer = None 159 | 160 | 161 | class AppBreathe(object): 162 | """ 163 | 定义App心跳行为,用于检测客户端是否仍然在连接(客户端循环请求发送心跳请求): 164 | 定时器 __seconds 检测一次,连续 __times 次没接收到心跳请求则判定客户端失去连接 165 | 166 | @author Myles Yang 167 | """ 168 | 169 | def __init__(self, interval: int = 60, times: int = 5): 170 | """ 171 | :param interval: 循环计时器间隔,单位秒 172 | :param times: 循环检测次数 173 | """ 174 | self.__timer: SimpleTaskTimer = None 175 | # 定时器循环频率 176 | self.__seconds: int = interval 177 | # 检测次数 178 | self.__times: int = times 179 | # 记录没有收到心跳信号次数 180 | self.__signals: int = 0 181 | 182 | def __action(self, callback): 183 | self.__signals += 1 184 | if self.__signals > self.__times: 185 | if callback and callable(callback): 186 | callback() 187 | 188 | def run(self, callback=None): 189 | """ 190 | 启动 191 | :param callback: 判定为失去连接时发生的回调 192 | """ 193 | if self.__timer: 194 | return 195 | self.__timer = SimpleTaskTimer() 196 | self.__timer.run(self.__seconds, self.__action, [callback]) 197 | 198 | def is_alive(self) -> bool: 199 | """ 客户端连接是否仍存活 """ 200 | return self.__signals <= self.__times 201 | 202 | def record_alive(self): 203 | """ 重置信号 """ 204 | self.__signals = 0 205 | return True 206 | 207 | 208 | class SimpleMmapActuator(object): 209 | """ 210 | 基于mmap内存通信的实时单指令无参执行器 211 | 212 | @author Myles Yang 213 | """ 214 | 215 | def __init__(self, mname: str = 'GlobalRandomDesktopBackgroundShareMemory', msize: int = 64): 216 | """ 217 | :param mname: 映射文件名称 218 | :param msize: 映射大小(字节) 219 | """ 220 | self.name = mname 221 | self.size = msize 222 | self.sm_rd: mmap = None 223 | self.sm_rd_timer: SimpleTaskTimer = None 224 | self.sm_rd_call_map: Dict[str, typing.Callable] = {} 225 | pass 226 | 227 | def run_monitor(self, check_seconds: float = 0.125): 228 | """ 229 | 监控命令 230 | 231 | :param cmd_func_map: 命令与执行函数的映射表 232 | :param check_seconds: 命令检查间隔 233 | """ 234 | if self.sm_rd: 235 | return 236 | self.sm_rd = mmap.mmap(-1, self.size, tagname=self.name, access=mmap.ACCESS_WRITE) 237 | self.__empty_sm() 238 | self.sm_rd_timer = SimpleTaskTimer() 239 | self.sm_rd_timer.run(check_seconds, self.__call_read) 240 | 241 | def __call_read(self): 242 | self.sm_rd.seek(0) 243 | cmd = self.sm_rd.read(self.size) 244 | cmd = str(cmd, encoding='utf-8').replace('\x00', '') 245 | if cmd: 246 | self.__empty_sm() 247 | func = self.sm_rd_call_map.get(cmd) 248 | if func and callable(func): 249 | func() 250 | 251 | def __empty_sm(self): 252 | self.sm_rd.seek(0) 253 | self.sm_rd.write(b'\x00' * self.size) 254 | self.sm_rd.flush() 255 | 256 | def set_cmd_func_map(self, cmd_func_map: Dict[str, typing.Callable]): 257 | self.sm_rd_call_map = cmd_func_map 258 | 259 | def append_cmd_func_map(self, cmd_func_map: Dict[str, typing.Callable]): 260 | self.sm_rd_call_map.update(cmd_func_map) 261 | 262 | def cancel_monitor(self): 263 | """ 取消监控 """ 264 | if not self.sm_rd: 265 | return 266 | self.sm_rd_timer.cancel() 267 | self.sm_rd.close() 268 | self.sm_rd = None 269 | self.sm_rd_call_map = None 270 | 271 | def send_command(self, command: str): 272 | """ 273 | 发送执行命令 274 | """ 275 | with mmap.mmap(-1, self.size, tagname=self.name, access=mmap.ACCESS_WRITE) as m: 276 | m.seek(0) 277 | m.write(bytes(command, encoding='utf-8')) 278 | m.flush() 279 | -------------------------------------------------------------------------------- /src/py/configurator.py: -------------------------------------------------------------------------------- 1 | # -*-coding:utf-8-*- 2 | 3 | """ 4 | 相关配置配置读写解析 5 | 6 | @author Myles Yang 7 | """ 8 | 9 | import os 10 | import urllib.request 11 | from typing import Dict, List, Tuple 12 | 13 | import const_config as const 14 | import dao 15 | import utils 16 | from vo import ConfigVO 17 | 18 | 19 | def get_bg_abspaths(): 20 | """ 21 | 获取壁纸目录下的绝对路径列表 22 | 23 | 不包括子目录 24 | 25 | :return: 壁纸绝对路径列表 26 | """ 27 | bg_srcpath = get_wallpaper_abspath() 28 | bg_paths = [] 29 | try: 30 | if os.path.exists(bg_srcpath) and os.path.isfile(bg_srcpath): 31 | os.remove(bg_srcpath) 32 | return bg_paths 33 | os.makedirs(bg_srcpath, exist_ok=True) 34 | 35 | for df in os.listdir(bg_srcpath): 36 | df_abspath = os.path.join(bg_srcpath, df) 37 | if os.path.isfile(df_abspath): 38 | bg_paths.append(df_abspath) 39 | except: 40 | pass 41 | 42 | return bg_paths 43 | 44 | 45 | def parse_config() -> Dict[str, ConfigVO]: 46 | """ 获取配置信息 """ 47 | return dao.list_config() 48 | 49 | 50 | """============================ [Run] ============================""" 51 | 52 | 53 | def get_workdir(config: Dict[str, ConfigVO] = None) -> str: 54 | """ 55 | 获取工作目录 56 | :return: 目录 57 | """ 58 | key = const.Key.Run.WORKDIR.value 59 | vo = config.get(key) if config else dao.get_config(key) 60 | return vo.value if vo and vo.value else '' 61 | 62 | 63 | def get_wallpaper_abspath(config: Dict[str, ConfigVO] = None) -> str: 64 | """ 壁纸保存目录 """ 65 | workdir = get_workdir(config) 66 | workdir = workdir if workdir else 'run' 67 | return os.path.abspath(os.path.join(workdir, const.bg_srcpath)) 68 | 69 | 70 | def get_log_abspath(config: Dict[str, ConfigVO] = None) -> str: 71 | """ 运行日志保存目录 """ 72 | workdir = get_workdir(config) 73 | workdir = workdir if workdir else 'run' 74 | return os.path.abspath(os.path.join(workdir, const.log_srcpath)) 75 | 76 | 77 | def get_favorite_abspath(config: Dict[str, ConfigVO] = None) -> str: 78 | """ 收藏文件夹 """ 79 | workdir = get_workdir(config) 80 | workdir = workdir if workdir else 'run' 81 | return os.path.abspath(os.path.join(workdir, const.favorite_srcpath)) 82 | 83 | 84 | def get_proxies(config: Dict[str, ConfigVO] = None) -> Dict[str, str]: 85 | """ 获取用户代理 86 | :return: 代理配置 87 | """ 88 | key = const.Key.Run.PROXY.value 89 | vo = config.get(key) if config else dao.get_config(key) 90 | proxy_name = vo.value if vo and vo.value else '' 91 | if proxy_name == const.Key.Run._PROXY_SYSTEM.value: 92 | return urllib.request.getproxies() 93 | return {} 94 | 95 | 96 | def get_rotation(config: Dict[str, ConfigVO] = None) -> str: 97 | """ 获取轮播方式 """ 98 | key = const.Key.Run.ROTATION.value 99 | vo = config.get(key) if config else dao.get_config(key) 100 | return vo.value if vo and vo.value else const.Key.Run._ROTATION_NETWORK.value 101 | 102 | 103 | def is_local_disorder(config: Dict[str, ConfigVO] = None) -> bool: 104 | """ 是否为本地轮播且为无序 """ 105 | key = const.Key.Run.LOCAL__DISORDER.value 106 | vo = config.get(key) if config else dao.get_config(key) 107 | rotation = get_rotation(config) 108 | return rotation == const.Key.Run._ROTATION_LOCAL.value and vo and vo.value 109 | 110 | 111 | """============================ [Api] ============================""" 112 | 113 | 114 | def get_api_name(config: Dict[str, ConfigVO] = None) -> str: 115 | """ 获取API名字 """ 116 | key = const.Key.Api.NAME.value 117 | vo = config.get(key) if config else dao.get_config(key) 118 | return vo.value if vo and vo.value else const.Key.Api._NAME_WALLHAVEN.value 119 | 120 | 121 | def get_wallhaven_url(config: Dict[str, ConfigVO] = None) -> str: 122 | """ 获取 WALLHAVEN URL """ 123 | key = const.Key.Api.WALLHAVEN__URL.value 124 | vo = config.get(key) if config else dao.get_config(key) 125 | return vo.value if vo and vo.value else vo.defaults 126 | 127 | 128 | def get_wallhaven_apikey(config: Dict[str, ConfigVO] = None) -> str: 129 | """ 获取 WALLHAVEN API KEY """ 130 | key = const.Key.Api.WALLHAVEN__APIKEY.value 131 | vo = config.get(key) if config else dao.get_config(key) 132 | return vo.value if vo and vo.value else '' 133 | 134 | 135 | def get_custom_urls(config: Dict[str, ConfigVO] = None) -> List[str]: 136 | """ 获取自定义图源URL """ 137 | key = const.Key.Api.CUSTOM.value 138 | config = config if config else dao.list_config(like_key='{}%'.format(key)) 139 | result = [] 140 | for k in config: 141 | if k and k.startswith(key): 142 | vo = config.get(k) 143 | if vo and vo.value: 144 | result.append(vo.value) 145 | return result 146 | 147 | 148 | """============================ [Task] ============================""" 149 | 150 | 151 | def get_current(config: Dict[str, ConfigVO] = None) -> int: 152 | """ 153 | 获取配置中当前壁纸数组的下标 154 | :return: 值不存在或小于0,返回0 155 | """ 156 | key = const.Key.Task.CURRENT.value 157 | vo = config.get(key) if config else dao.get_config(key) 158 | return vo.value if vo and vo.value else 0 159 | 160 | 161 | def set_current(current: int = 0) -> int: 162 | """ 163 | 更新配置文件中当前壁纸数组的下标 164 | """ 165 | vo = ConfigVO(const.Key.Task.CURRENT.value, current) 166 | return dao.update_config([vo], True) 167 | 168 | 169 | def get_seconds(config: Dict[str, ConfigVO] = None) -> int: 170 | """ 171 | 获取桌面背景更换的频率 172 | :return: 值不存在或小于10,返回300 173 | """ 174 | key = const.Key.Task.SECONDS.value 175 | vo = config.get(key) if config else dao.get_config(key) 176 | return vo.value if vo and vo.value else 600 177 | 178 | 179 | def get_task_mode(config: Dict[str, ConfigVO] = None) -> str: 180 | """ 获取任务模式:多张 / 一张 """ 181 | key = const.Key.Task.MODE.value 182 | vo = config.get(key) if config else dao.get_config(key) 183 | return vo.value if vo and vo.value else const.Key.Task._MODE_MULTIPLE.value 184 | 185 | 186 | def get_dwn_threads(config: Dict[str, ConfigVO] = None) -> int: 187 | """ 188 | 获取下载壁纸时的线程数 189 | """ 190 | key = const.Key.Task.THREADS.value 191 | vo = config.get(key) if config else dao.get_config(key) 192 | return vo.value if vo and vo.value else min(32, (os.cpu_count() or 1) + 4) 193 | 194 | 195 | def get_random_sleep(config: Dict[str, ConfigVO] = None) -> Tuple[float, float]: 196 | """ 197 | 获取在下载壁纸前的随机睡眠时间 198 | :return tuple: 返回两个元素的元组,生成两个数间的随机值 199 | """ 200 | l_key = const.Key.Task.RND_SLEEP_L.value 201 | r_key = const.Key.Task.RND_SLEEP_R.value 202 | L = config.get(l_key) if config else dao.get_config(l_key) 203 | R = config.get(r_key) if config else dao.get_config(r_key) 204 | 205 | lval = L.value if L and L.value else 0.5 206 | rval = R.value if R and R.value else 5 207 | 208 | # 判断两个值大小是否反了 209 | if lval > rval: 210 | tmp = rval 211 | rval = lval 212 | lval = tmp 213 | 214 | return lval, rval 215 | 216 | 217 | def is_retain_bg_files(config: Dict[str, ConfigVO] = None) -> bool: 218 | """ 219 | 在拉取新的壁纸前,是否保留旧的壁纸 220 | """ 221 | key = const.Key.Task.RETAIN_BGS.value 222 | vo = config.get(key) if config else dao.get_config(key) 223 | return vo and vo.value 224 | 225 | 226 | def get_max_retain_bg_mb(config: Dict[str, ConfigVO] = None) -> int: 227 | """ 228 | 获取壁纸保留最大占用存储空间 229 | """ 230 | key = const.Key.Task.MAX_RETAIN_MB.value 231 | vo = config.get(key) if config else dao.get_config(key) 232 | return vo.value if vo and vo.value else 0 233 | 234 | 235 | """============================ [Hotkey] ============================""" 236 | 237 | 238 | def is_hotkey_enabled(config: Dict[str, ConfigVO] = None) -> bool: 239 | """ 240 | 是否启用热键 241 | """ 242 | key = const.Key.Hotkey.ENABLE.value 243 | vo = config.get(key) if config else dao.get_config(key) 244 | return vo and vo.value 245 | 246 | 247 | def get_hotkey_prev(config: Dict[str, ConfigVO] = None) -> List[str]: 248 | """ 249 | 获取热键:上一个壁纸 250 | """ 251 | key = const.Key.Hotkey.PREV_BG.value 252 | vo = config.get(key) if config else dao.get_config(key) 253 | return __get_hotkey(vo) 254 | 255 | 256 | def get_hotkey_next(config: Dict[str, ConfigVO] = None) -> List[str]: 257 | """ 258 | 获取热键:下一个壁纸 259 | """ 260 | key = const.Key.Hotkey.NEXT_BG.value 261 | vo = config.get(key) if config else dao.get_config(key) 262 | return __get_hotkey(vo) 263 | 264 | 265 | def get_hotkey_favorite(config: Dict[str, ConfigVO] = None) -> List[str]: 266 | """ 267 | 获取热键:收藏当前壁纸 268 | """ 269 | key = const.Key.Hotkey.FAV_BG.value 270 | vo = config.get(key) if config else dao.get_config(key) 271 | return __get_hotkey(vo) 272 | 273 | 274 | def get_hotkey_locate(config: Dict[str, ConfigVO] = None) -> List[str]: 275 | """ 276 | 获取热键:定位到当前壁纸 277 | """ 278 | key = const.Key.Hotkey.LOC_BG.value 279 | vo = config.get(key) if config else dao.get_config(key) 280 | return __get_hotkey(vo) 281 | 282 | 283 | def __get_hotkey(config: ConfigVO) -> List[str]: 284 | """ 285 | 获取热键通用方法 286 | """ 287 | if not config or not config.value: 288 | return [] 289 | hk = utils.get_split_value(config.value, '+', opt_type=str) 290 | return utils.list_deduplication(hk) 291 | 292 | 293 | """============================ [CtxMenu] ============================""" 294 | 295 | 296 | def is_ctxmenu_enabled(config: Dict[str, ConfigVO] = None) -> bool: 297 | """ 298 | 是否启用桌面右键菜单 299 | """ 300 | key = const.Key.CtxMenu.ENABLE.value 301 | vo = config.get(key) if config else dao.get_config(key) 302 | return vo and vo.value 303 | 304 | 305 | def is_ctxmenu_prev_enabled(config: Dict[str, ConfigVO] = None) -> bool: 306 | """ 307 | 是否启用桌面右键菜单 308 | """ 309 | key = const.Key.CtxMenu.PREV_BG.value 310 | vo = config.get(key) if config else dao.get_config(key) 311 | return vo and vo.value 312 | 313 | 314 | def is_ctxmenu_next_enabled(config: Dict[str, ConfigVO] = None) -> bool: 315 | """ 316 | 是否启用切换下一张壁纸右键菜单 317 | """ 318 | key = const.Key.CtxMenu.NEXT_BG.value 319 | vo = config.get(key) if config else dao.get_config(key) 320 | return vo and vo.value 321 | 322 | 323 | def is_ctxmenu_locate_enabled(config: Dict[str, ConfigVO] = None) -> bool: 324 | """ 325 | 是否启用收藏当前壁纸右键菜单 326 | """ 327 | key = const.Key.CtxMenu.FAV_BG.value 328 | vo = config.get(key) if config else dao.get_config(key) 329 | return vo and vo.value 330 | 331 | 332 | def is_ctxmenu_favorite_enabled(config: Dict[str, ConfigVO] = None) -> bool: 333 | """ 334 | 是否启用定位到当前壁纸文件右键菜单 335 | """ 336 | key = const.Key.CtxMenu.LOC_BG.value 337 | vo = config.get(key) if config else dao.get_config(key) 338 | return vo and vo.value 339 | 340 | 341 | """============================ PID ============================""" 342 | 343 | 344 | def record_pid(ptype: str, running: bool = True) -> int: 345 | """ 346 | 记录程序运行的PID 347 | """ 348 | pid = os.getpid() if running else -1 349 | if ptype == 'bpid': 350 | i = dao.update_config([ConfigVO(const.Key.Run.BPID.value, pid)], True) 351 | elif ptype == 'fpid': 352 | i = dao.update_config([ConfigVO(const.Key.Run.FPID.value, pid)], True) 353 | return pid 354 | 355 | 356 | def record_bpid(running: bool = True) -> int: 357 | return record_pid('bpid', running) 358 | 359 | 360 | def record_fpid(running: bool = True) -> int: 361 | return record_pid('fpid', running) 362 | 363 | 364 | def get_pid(ptype: str) -> int: 365 | """ 366 | 获取PID 367 | :param ptype: bpid | fpid 368 | :return int: 返回PID,获取失败返回-1 369 | """ 370 | if ptype == 'bpid': 371 | config = dao.get_config(const.Key.Run.BPID.value) 372 | elif ptype == 'fpid': 373 | config = dao.get_config(const.Key.Run.FPID.value) 374 | else: 375 | return -1 376 | 377 | if config and config.value: 378 | return int(config.value) 379 | else: 380 | return -1 381 | 382 | 383 | def get_bpid() -> int: 384 | """ 获取后台运行程序ID """ 385 | return get_pid('bpid') 386 | 387 | 388 | def get_fpid() -> int: 389 | """ 获取前台运行程序ID(webui服务) """ 390 | return get_pid('fpid') 391 | -------------------------------------------------------------------------------- /src/py/const_config.py: -------------------------------------------------------------------------------- 1 | # -*-coding:utf-8-*- 2 | 3 | """ 4 | 常量定义 5 | 6 | @author Myles Yang 7 | """ 8 | import os 9 | import sys 10 | from enum import Enum 11 | 12 | # 程序名称 13 | app_name = '随机桌面壁纸' 14 | app_name_en = "RandomDesktopBackground" 15 | 16 | # webui 地址 17 | host = '127.6.6.6' 18 | 19 | # webui 端口 20 | port = 23333 21 | 22 | # webui服务路径 23 | server = 'http://{}:{}'.format(host, port) 24 | 25 | # 对话框标题 26 | dialog_title = '来自"{}"的提示'.format(app_name) 27 | 28 | app_fullpath = os.path.abspath(sys.argv[0]) 29 | app_dirname = os.path.dirname(app_fullpath) 30 | 31 | # 壁纸保存目录 32 | bg_srcpath = 'wallpapers' 33 | 34 | # 运行日志保存目录 35 | log_srcpath = 'log' 36 | 37 | # 收藏文件夹 38 | favorite_srcpath = 'favorite' 39 | 40 | # 数据库名称 41 | db_name = 'rdbdb.db' 42 | 43 | 44 | class Key: 45 | """ 配置key定义 """ 46 | 47 | class Run(Enum): 48 | BPID = 'run.bpid' 49 | FPID = 'run.fpid' 50 | WORKDIR = 'run.workdir' 51 | STARTUP = 'run.startup' 52 | 53 | PROXY = 'run.proxy' 54 | _PROXY_NONE = 'none' 55 | _PROXY_SYSTEM = 'system' 56 | 57 | ROTATION = 'run.rotation' 58 | _ROTATION_LOCAL = 'local' 59 | _ROTATION_NETWORK = 'network' 60 | LOCAL__DISORDER = 'run.local.disorder' 61 | 62 | class Api(Enum): 63 | NAME = 'api.name' 64 | 65 | WALLHAVEN__URL = 'api.wallhaven.url' 66 | WALLHAVEN__APIKEY = 'api.wallhaven.apikey' 67 | 68 | CUSTOM = 'api.custom.url' 69 | _NAME_WALLHAVEN = 'wallhaven' 70 | _NAME_CUSTOM = 'custom' 71 | 72 | class Task(Enum): 73 | SECONDS = 'task.seconds' 74 | CURRENT = 'task.current' 75 | MODE = 'task.mode' 76 | THREADS = 'task.threads' 77 | RND_SLEEP_L = 'task.rnd_sleep_l' 78 | RND_SLEEP_R = 'task.rnd_sleep_r' 79 | RETAIN_BGS = 'task.retain_bgs' 80 | 81 | MAX_RETAIN_MB = 'task.max_retain_mb' 82 | _MODE_MULTIPLE = 'multiple' 83 | _MODE_SINGLE = 'single' 84 | 85 | class Hotkey(Enum): 86 | ENABLE = 'hotkey.enable' 87 | PREV_BG = 'hotkey.prev_bg' 88 | NEXT_BG = 'hotkey.next_bg' 89 | FAV_BG = 'hotkey.fav_bg' 90 | LOC_BG = 'hotkey.loc_bg' 91 | 92 | class CtxMenu(Enum): 93 | ENABLE = 'ctxmenu.enable' 94 | PREV_BG = 'ctxmenu.prev_bg' 95 | NEXT_BG = 'ctxmenu.next_bg' 96 | FAV_BG = 'ctxmenu.fav_bg' 97 | LOC_BG = 'ctxmenu.loc_bg' 98 | 99 | 100 | # wallhaven请求api 101 | wallhaven_api = 'https://wallhaven.cc/api/v1/search' 102 | # wallhaven网站 103 | wallhaven_website = 'https://wallhaven.cc/search' 104 | 105 | # 请求头 106 | headers = { 107 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.26 Safari/537.36 Core/1.63.6776.400 QQBrowser/10.3.2577.400' 108 | } 109 | 110 | # favicon 111 | favicon = 'AAABAAEAICAAAAEAIACoEAAAFgAAACgAAAAgAAAAQAAAAAEAIAAAAAAAgBAAABMLAAATCwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+fQir/nT+d/55Apv+eQJ7/nkCe/55Anv+eQJ7/nkCe/55Anv+eQJ7/nkCe/59Anf+fQJ3/n0Cd/59Anf+fQJ3/n0Cd/59Anf+fQJ3/n0Cl/55Anf+fQCwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/oEJZ/59B5/+eQP//nkD//55A//+eQP//nkD//55A//+eQP//nkD//55A//+eQP//nkD//55A//+eQP//nkD//55A//+eQP//nkD//55A//+eQP//nkD//55A4f+fQFIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+dP5H/nkD//55A//+eQP//nkD//55A//+eQP//nkD//55A//+eQP//nkD//55A//+eQP//nkD//55A//+eQP//nkD//55A//+eQP//nkD//55A//+eQP//nkD//55AjwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/55AkP+eQP//n0D//55A//+eQP//nkD//55A//+eQP//nkD//55A//+eQP//nkD//55A//+eQP//nkD//55A//+eQP//nkD//55A//+eQP//nkD//55A//+eQP//nkCRAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/nkCZ/55A//+dQeL/nkD//55A//+eQP//nkD//55A//+eQP//nkD//55A//+eQP//nkD//55A//+eQP//nkD//55A//+eQP//nkD//55A//+eQP//nUDi/55A//+eQJkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+fP6r/oDz//5dNVP+aSK7/nz3//55A//+eQP//nkD//55A//+eQP//nkD//55A//+eQP//nkD//55A//+eQP//nkD//55A//+eQP//mkP//6k5r/+rN1X/mUP//55AqgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/55Ap/+eP///m0M7/59BDv+gPf//nkD//55A//+eQP//nj///58+7/+eQPf/nkD//55A//+eQP//nkD//55A//+eQP//nkD//55A//+eQ///oT8R/589Ov+dQf//nkCnAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/nkCn/55A//+cPlkAAAAA/51Ge/+eP///nkD//55A//+eQej/nUMJ/54/mf+eQP//nkD//55A//+eQP//nkD//55A//+eQP//nkD//547fwAAAAD/mkJZ/59A//+eQKcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+eQKf/nkD//5w+WwAAAAAAAAAA/58/wf+eQP//nz7//5pFMgAAAAD/okAW/55A8P+eQP//nkD//55A//+eQP//nkD//55A//+eQMQAAAAAAAAAAP+cQlv/nkD//55ApwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/55Ap/+eQP//m0BbAAAAAAAAAAD/oEAh/55Alv+fPzcAAAAAAAAAAAAAAAD/oEJV/54///+eQP//nkD//55A//+eQP//nkD8/6BCJgAAAAAAAAAA/5xAW/+eQP//nkCnAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/nkCn/55A//+cQFsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/oUCZ/55A//+eQP//nkD//55A//+eQGYAAAAAAAAAAAAAAAD/nEBb/55A//+eQKcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+eQKf/nkD//5xAWwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+GQAL/nUDZ/6BA//+eQP//n0GqAAAAAAAAAAAAAAAAAAAAAP+cQFv/nkD//55ApwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/55Ap/+eQP//m0BbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+dQTX/n0D//55A7P+fQwwAAAAA/51AAf+eQQMAAAAA/5tAW/+eQP//nkCnAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/nkCn/55A//+bQFsAAAAA/6BBLf+eQf//n0LT/54+BgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+fPxj/nkAK/51ASP+ePyD/n0Al/59BvQAAAAD/m0BJ/55A//+eQKcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+eQKf/nkD//5xAVQAAAAD/nkHa/54/wP+eQeD/nDykAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/m0Ak/54/mf+eQIv/n0JjAAAAAP+bQFP/nkD//55ApwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/55Ap/+eQP//m0BXAAAAAP+fQbL/nkD8/54///+eQnoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+dQUD/n0BX/59AWv+eQZ0AAAAA/5xATf+eQP//nkCnAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/nkCr/55A//+bQFYAAAAA/55AA/+cQJ3/nEB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/55AG/+gQQL/n0AF/58/UQAAAAD/m0BP/55A//+eQKsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+cP6j/nkD//5xAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+dQF//nkD//59CqQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/6ZDXf+fQOr/nT///55A//+eQP//nkD//55A//+eQP//nkD//55A//+eQP//nkD//55A//+eQP//nkD//55A//+eQP//nkD//55A//+eQP//nkD//55B//+dP+n/nTxcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/59BLf+cQLX/nkDG/55Au/+eQLv/nkC7/55Au/+eQLv/nkC7/55Au/+eQLv/nkC7/55Au/+eQLv/nkC7/55Au/+eQLv/nkC7/55Au/+eQMb/oEK1/50/LwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////////////////////////////////AAAP/gAAB/4AAAf+AAAH/gAAB/4AAAf+wAA3/sEAN/5jgGf+d8Dn/n/A5/5/4ef+f/Pn/mf/t/7D/jf+w/43/uf/9/5//+f+AAAH/wAAD////////////////////////////////8=' 112 | 113 | # 数据库创建SQL 114 | db_init_sql = R""" 115 | PRAGMA foreign_keys = false; 116 | 117 | -- ---------------------------- 118 | -- Table structure for config 119 | -- ---------------------------- 120 | DROP TABLE IF EXISTS "config"; 121 | CREATE TABLE "config" ( 122 | "key" text(127) NOT NULL, 123 | "value" text DEFAULT '', 124 | "pytype" text(15) DEFAULT 'str', 125 | "defaults" text DEFAULT NULL, 126 | "comment" text DEFAULT '', 127 | "enable" integer(1) DEFAULT 1, 128 | "utime" char(19) DEFAULT (datetime('now','localtime')), 129 | "ctime" char(19) DEFAULT (datetime('now','localtime')), 130 | PRIMARY KEY ("key") 131 | ); 132 | 133 | -- ---------------------------- 134 | -- Records of config 135 | -- ---------------------------- 136 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('run.bpid', '-1', 'int', 'b''\x80\x04\x95\x06\x00\x00\x00\x00\x00\x00\x00J\xff\xff\xff\xff.''', '运行:后台运行程序的PID'); 137 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('run.fpid', '-1', 'int', 'b''\x80\x04\x95\x06\x00\x00\x00\x00\x00\x00\x00J\xff\xff\xff\xff.''', '运行:前台运行程序的PID'); 138 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('run.startup', 'False', 'bool', 'b''\x80\x04\x89.''', '运行:是否开机启动'); 139 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('run.workdir', '', 'str', 'b''\x80\x04\x95\x04\x00\x00\x00\x00\x00\x00\x00\x8c\x00\x94.''', '运行:程序工作目录'); 140 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('run.proxy', 'none', 'str', 'b''\x80\x04\x95\n\x00\x00\x00\x00\x00\x00\x00\x8c\x06system\x94.''', '运行:用户代理[none|system]'); 141 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('run.rotation', 'network', 'str', 'b''\x80\x04\x95\x0b\x00\x00\x00\x00\x00\x00\x00\x8c\x07network\x94.''', '运行:轮播方式[network|local]'); 142 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('run.local.disorder', 'True', 'bool', 'b''\x80\x04\x88.''', '运行:本地轮播是否为无序'); 143 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('api.name', 'wallhaven', 'str', 'b''\x80\x04\x95\r\x00\x00\x00\x00\x00\x00\x00\x8c\twallhaven\x94.''', 'API:图源名[wallhaven|custom]'); 144 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('api.wallhaven.url', 'https://wallhaven.cc/search?categories=111&purity=110&sorting=random&order=desc&resolutions=1920x1080', 'str', 'b''\x80\x04\x95S\x00\x00\x00\x00\x00\x00\x00\x8cOhttps://wallhaven.cc/search?categories=111&purity=110&sorting=random&order=desc&resolutions=1920x1080\x94.''', 'API:wallhaven图源地址'); 145 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('api.wallhaven.apikey', '', 'str', 'b''\x80\x04\x95\x04\x00\x00\x00\x00\x00\x00\x00\x8c\x00\x94.''', 'API:wallhaven API Key'); 146 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('api.custom.url--536007639', 'https://api.btstu.cn/sjbz/?lx=suiji', 'str', NULL, ''); 147 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('api.custom.url--542024925', 'https://api.btstu.cn/sjbz/?lx=meizi', 'str', NULL, ''); 148 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('api.custom.url--302158599', 'https://api.btstu.cn/sjbz/?lx=dongman', 'str', NULL, ''); 149 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('task.seconds', '1440', 'int', 'b''\x80\x04\x95\x04\x00\x00\x00\x00\x00\x00\x00M\xa0\x05.''', '任务:切换频率'); 150 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('task.current', '0', 'int', 'b''\x80\x04K\x00.''', '任务:当前壁纸下标'); 151 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('task.mode', 'multiple', 'str', 'b''\x80\x04\x95\x0c\x00\x00\x00\x00\x00\x00\x00\x8c\x08multiple\x94.''', '任务:壁纸拉取模式[multiple|single]'); 152 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('task.threads', '2', 'int', 'b''\x80\x04K\x02.''', '任务:壁纸拉取时使用线程数量'); 153 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('task.rnd_sleep_l', '0.5', 'float', 'b''\x80\x04\x95\n\x00\x00\x00\x00\x00\x00\x00G?\xe0\x00\x00\x00\x00\x00\x00.''', '任务:壁纸拉取时随机停顿间隔时间左'); 154 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('task.rnd_sleep_r', '5', 'int', 'b''\x80\x04K\x05.''', '任务:壁纸拉取时随机停顿间隔时间右'); 155 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('task.retain_bgs', 'False', 'bool', 'b''\x80\x04\x89.''', '任务:是否保留已经拉取下来的壁纸'); 156 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('task.max_retain_mb', '-1', 'int', 'b''\x80\x04\x95\x06\x00\x00\x00\x00\x00\x00\x00J\xff\xff\xff\xff.''', '任务:壁纸保留最大占用内存'); 157 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('hotkey.enable', 'False', 'bool', 'b''\x80\x04\x89.''', '热键:是否启用全局热键'); 158 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('hotkey.prev_bg', '', 'str', 'b''\x80\x04\x95\x04\x00\x00\x00\x00\x00\x00\x00\x8c\x00\x94.''', '热键:切换到上一张壁纸'); 159 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('hotkey.next_bg', '', 'str', 'b''\x80\x04\x95\x04\x00\x00\x00\x00\x00\x00\x00\x8c\x00\x94.''', '热键:切换到下一张壁纸'); 160 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('hotkey.fav_bg', '', 'str', 'b''\x80\x04\x95\x04\x00\x00\x00\x00\x00\x00\x00\x8c\x00\x94.''', '热键:收藏当前壁纸'); 161 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('hotkey.loc_bg', '', 'str', 'b''\x80\x04\x95\x04\x00\x00\x00\x00\x00\x00\x00\x8c\x00\x94.''', '热键:定位到当前壁纸文件'); 162 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('ctxmenu.enable', 'False', 'bool', 'b''\x80\x04\x89.''', '桌面右键菜单:是否启用桌面右键菜单'); 163 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('ctxmenu.prev_bg', 'False', 'bool', 'b''\x80\x04\x89.''', '桌面右键菜单:是否启用切换上一张壁纸'); 164 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('ctxmenu.next_bg', 'False', 'bool', 'b''\x80\x04\x89.''', '桌面右键菜单:是否启用切换下一张壁纸'); 165 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('ctxmenu.fav_bg', 'False', 'bool', 'b''\x80\x04\x89.''', '桌面右键菜单:是否启用收藏当前壁纸'); 166 | INSERT INTO config ("key", value, pytype, "defaults", comment) VALUES('ctxmenu.loc_bg', 'False', 'bool', 'b''\x80\x04\x89.''', '桌面右键菜单:是否启用定位到当前壁纸文件'); 167 | 168 | -- ---------------------------- 169 | -- Indexes structure for table config 170 | -- ---------------------------- 171 | CREATE UNIQUE INDEX "idx_key" 172 | ON "config" ( 173 | "KEY" ASC 174 | ); 175 | 176 | -- ---------------------------- 177 | -- Triggers structure for table config 178 | -- ---------------------------- 179 | CREATE TRIGGER "config.onupdate" 180 | AFTER UPDATE 181 | ON "config" 182 | BEGIN 183 | update config 184 | set UTIME = datetime('now','localtime') 185 | where key = (case when old.key = new.key then old.key else new.key end); 186 | END; 187 | 188 | PRAGMA foreign_keys = true; 189 | """ 190 | -------------------------------------------------------------------------------- /src/py/controller.py: -------------------------------------------------------------------------------- 1 | # -*-coding:utf-8-*- 2 | 3 | """ 4 | 控制器 5 | 6 | @author Myles Yang 7 | """ 8 | 9 | import base64 10 | import io 11 | import json 12 | import os 13 | 14 | import win32con 15 | from fastapi import APIRouter 16 | from starlette.requests import Request 17 | from starlette.responses import StreamingResponse, RedirectResponse 18 | 19 | import application 20 | import configurator as configr 21 | import const_config as const 22 | import dao 23 | import service 24 | import utils 25 | from component import AppBreathe, CustomLogger 26 | from vo import R 27 | 28 | log = CustomLogger().get_logger() 29 | 30 | # 路由根路径 31 | index = APIRouter() 32 | 33 | # 路由 api 34 | api = APIRouter(prefix="/api") 35 | 36 | app_breathe: AppBreathe = None # 服务端心跳维持 37 | 38 | 39 | def init_app_breathe(): 40 | """ 初始化 app 心跳行为,在webui服务启动时初始化 """ 41 | log.info("初始化 App 心跳行为") 42 | 43 | def cb(): 44 | log.info('WEBUI长时间无操作,自动退出程序') 45 | configr.record_fpid(False) 46 | utils.kill_process([os.getpid()]) 47 | 48 | global app_breathe 49 | app_breathe = AppBreathe(60, 5) 50 | app_breathe.run(callback=cb) 51 | return app_breathe 52 | 53 | 54 | """===================== INDEX =====================""" 55 | 56 | 57 | @index.get("/favicon.ico") 58 | async def favicon(): 59 | return StreamingResponse( 60 | content=io.BytesIO(base64.b64decode(const.favicon)), 61 | media_type='image/x-icon' 62 | ) 63 | 64 | 65 | @index.get('/exit') 66 | async def exit_webui(): 67 | log.info('主动关闭WEBUI服务') 68 | configr.record_fpid(False) 69 | msg = utils.kill_process([os.getpid()]) 70 | return R().ok(message='结束WEBUI服务端进程', data=msg) 71 | 72 | 73 | @index.get("/") 74 | async def home(): 75 | return RedirectResponse('/webui') 76 | 77 | 78 | @index.get("/webui") 79 | async def webui(): 80 | if not os.path.exists(os.path.join(const.app_dirname, 'webui', 'index.html')): 81 | utils.create_dialog("WEBUI组件缺失!", const.dialog_title, 82 | style=win32con.MB_ICONERROR, interval=7, 83 | callback=lambda x: utils.kill_process([os.getpid()])) 84 | return R().err('WEBUI组件缺失') 85 | return RedirectResponse('/webui/index.html') 86 | 87 | 88 | """===================== API =====================""" 89 | 90 | 91 | @api.get("/breathe") 92 | async def breathe(): 93 | """ APP心跳 """ 94 | return R().ok(message='APP心跳', data=app_breathe.record_alive()) 95 | 96 | 97 | @api.get("/config") 98 | async def get_config(): 99 | """ 获取配置信息 """ 100 | config = dao.list_config_kv() 101 | return R().ok(message='获取配置信息', data=config) 102 | 103 | 104 | @api.post("/config") 105 | async def update_config(request: Request): 106 | """ 更新配置信息 """ 107 | json = await request.json() 108 | service.update_config(json) 109 | return R().ok(message='更新配置信息') 110 | 111 | 112 | @api.get("/toggle-ud") 113 | async def toggle_ud(): 114 | """ toggle startup-shutdown 切换程序开关 """ 115 | status = service.toggle_startup_shutdown() 116 | return R().ok(message='程序状态信息', data=status.__dict__) 117 | 118 | 119 | @api.get("/config/workdir") 120 | def get_workdir(): 121 | """ 选择工作目录 """ 122 | path = utils.select_folder('请选择工作目录') 123 | return R().ok('工作目录', path) 124 | 125 | 126 | @api.get("/status") 127 | async def get_status(): 128 | """ 获取状态信息 """ 129 | status = service.get_status() 130 | return R().ok(message='程序状态信息', data=status.__dict__) 131 | 132 | 133 | @api.get("/create-desktop-lnk") 134 | async def create_desktop_lnk(): 135 | """ 创建程序桌面快捷方式 """ 136 | utils.create_shortcut(application.app_fullpath, 'shell:desktop:{}'.format(const.app_name), style=7) 137 | return R().ok(message='创建桌面快捷方式') 138 | 139 | 140 | @api.get("/locate-favorite-path") 141 | async def locate_favorite_path(): 142 | """ 打开收藏文件夹 """ 143 | service.locate_favorite_path() 144 | return R().ok(message='打开收藏文件夹') 145 | 146 | 147 | @api.get("/locate-workdir") 148 | async def locate_workdir(): 149 | """ 打开工作目录 """ 150 | service.locate_workdir() 151 | return R().ok(message='打开工作目录') 152 | 153 | 154 | @api.post("/wallhaven") 155 | async def get_wallhaven(request: Request): 156 | """ wallhaven接口数据,因为前端直接请求会产生跨域,直接后端请求返回 """ 157 | params = await request.json() 158 | resp = utils.simple_request_get(const.wallhaven_api, params=params) 159 | result = resp.text 160 | if resp.status_code == 200: 161 | return R().ok(message='wallhaven数据', data=json.loads(result)) 162 | else: 163 | return R().err(message=result) 164 | -------------------------------------------------------------------------------- /src/py/dao.py: -------------------------------------------------------------------------------- 1 | # -*-coding:utf-8-*- 2 | 3 | """ 4 | 持久层 5 | 6 | @author Myles Yang 7 | """ 8 | import os 9 | import re 10 | import sqlite3 11 | import typing 12 | from sqlite3 import Cursor 13 | from typing import Union, List, Dict 14 | 15 | import const_config as const 16 | import utils 17 | from vo import E, ConfigVO 18 | 19 | 20 | def run_sql(cb): 21 | """ 22 | 执行 SQL 23 | :param cb: 回调函数,参数 Cursor 24 | :return: None 25 | """ 26 | 27 | con = None 28 | cur = None 29 | try: 30 | con = sqlite3.connect(os.path.join(const.app_dirname, const.db_name)) 31 | cur = con.cursor() 32 | res = cb(cur) 33 | con.commit() 34 | return res 35 | except Exception as e: 36 | if con: 37 | con.rollback() 38 | raise E().e(e, message="读取或修改配置信息错误") 39 | finally: 40 | if cur: 41 | cur.close() 42 | if con: 43 | con.close() 44 | 45 | 46 | def init_db(): 47 | """ 初始化默认数据库 """ 48 | utils.rm_file_dir(const.db_name) 49 | with open(const.db_name, 'wb'): 50 | pass 51 | 52 | def cb(cur: Cursor): 53 | cur.executescript(const.db_init_sql) 54 | return cur.rowcount 55 | 56 | return run_sql(cb) 57 | 58 | 59 | def __str_escape(value: str): 60 | """ SQL 特殊字符转义 """ 61 | value = str(value) 62 | value = value.replace("'", "''") 63 | # value = value.replace("/", "//") 64 | # value = value.replace("[", "/[") 65 | # value = value.replace("]", "/]") 66 | # value = value.replace("%", "/%") 67 | # value = value.replace("&", "/&") 68 | # value = value.replace("_", "/_") 69 | # value = value.replace("(", "/(") 70 | # value = value.replace(")", "/)") 71 | return value 72 | 73 | 74 | def __get_pytype(pytype: str): 75 | """ 转换失败返回None """ 76 | tpy = [None] 77 | try: 78 | if pytype and re.match('^(int|float|bool|complex|str|tuple|list|set|dict|type)$', pytype): 79 | exec('tpy[0]={}'.format(pytype)) 80 | else: 81 | raise Exception('转换失败') 82 | except: 83 | tpy = [None] 84 | return tpy[0] 85 | 86 | 87 | def __is_direct_type(typename: str): 88 | return typename and re.match('^(int|float|complex|bool|str)$', typename) 89 | 90 | 91 | def __get_true_value(key: str, str_value: str, pytype: Union[type, str] = None) -> typing.Any: 92 | """ 获取配置健的真实值 """ 93 | try: 94 | if pytype is None: 95 | tpy = str 96 | # 基本数值类型,直接转换 97 | elif isinstance(pytype, type) and __is_direct_type(pytype.__name__): 98 | tpy = pytype 99 | else: 100 | tpy = __get_pytype(pytype.__name__ if isinstance(pytype, type) else str(pytype)) 101 | 102 | if not isinstance(str_value, str): 103 | result = str_value 104 | elif tpy == bool: 105 | result = str_value.lower() == 'true' 106 | elif tpy is not None and re.match('^(int|float|complex|bool|str)$', tpy.__name__): 107 | result = tpy(str_value) 108 | else: 109 | try: 110 | result = utils.deserialize(str_value) 111 | except: 112 | result = str(str_value) 113 | except Exception as e: 114 | raise E().e(e, message='获取配置健的真实值错误,k[{}],v[{}],t[{}]'.format(key, str_value, pytype)) 115 | return result 116 | 117 | 118 | def __get_result(result: dict) -> ConfigVO: 119 | """ 获取结果集,转成ConfigVO """ 120 | result = result if result else {} 121 | vo = ConfigVO() 122 | 123 | key = str(result.get('key')) 124 | value = str(result.get('value')) 125 | enable = str(result.get('enable')) 126 | defaults = result.get('defaults') 127 | 128 | vo.key = key 129 | vo.value = __get_true_value(key, value, result.get('pytype')) 130 | vo.pytype = __get_pytype(result.get('pytype')) 131 | vo.defaults = utils.deserialize(defaults) if defaults is not None else defaults 132 | vo.enable = enable and enable != '0' 133 | vo.comment = result.get('comment') 134 | vo.utime = result.get('utime') 135 | vo.ctime = result.get('ctime') 136 | return vo 137 | 138 | 139 | def kv2dict(data: dict) -> dict: 140 | """ 141 | 数据库key-value转字典,如{'run.api': ''} -> {'run':{api:''}} 142 | :param data: 数据库查询出来的键值对 143 | :return: 144 | """ 145 | res = {} 146 | try: 147 | for key in data: 148 | value = data.get(key) 149 | get_statement = 'res' 150 | set_statement = 'res' 151 | for dict_key in key.split('.'): 152 | get_statement += ".get('{}')".format(dict_key) 153 | set_statement += "['{}']".format(dict_key) 154 | if not eval(get_statement): 155 | exec(set_statement + '={}') 156 | exec(set_statement + '=value') 157 | except Exception as e: 158 | raise E().e(e, message='数据转换出错') 159 | return res 160 | 161 | 162 | def dict2kv(data: dict) -> dict: 163 | """ 164 | 字典转数据库key-value,如{'run':{api:''}} -> {'run.api': ''} 165 | :param data: 前端传回来的配置项 166 | :return: 167 | """ 168 | 169 | def recursion(key: str, rdt: dict, res: dict): 170 | for k in rdt: 171 | cur_key = key + ('.' if key else '') + k 172 | if isinstance(rdt[k], dict): 173 | recursion(cur_key, rdt[k], res) 174 | else: 175 | res[cur_key] = rdt[k] 176 | 177 | result = {} 178 | try: 179 | recursion('', data, result) 180 | except Exception as e: 181 | raise E().e(e, message='数据转换出错') 182 | return result 183 | 184 | 185 | def list_config(enable: bool = None, like_key: str = None) -> Dict[str, ConfigVO]: 186 | """ 187 | 列出所有配置信息 188 | :return: {k:ConfigVO,} 189 | """ 190 | 191 | def cb(cur: Cursor): 192 | sql = "select key, value, pytype, defaults, enable, comment, utime, ctime from config where 1=1 {} {};".format( 193 | "and enable = {}".format(1 if enable else 0) if enable is not None else "", 194 | "and key like '{}'".format(like_key) if like_key else "" 195 | ) 196 | cur.execute(sql) 197 | res = {} 198 | for row in cur.fetchall(): 199 | res[row[0]] = __get_result({ 200 | 'key': row[0], 201 | 'value': row[1], 202 | 'pytype': row[2], 203 | 'default': row[3], 204 | 'enable': row[4], 205 | 'comment': row[5], 206 | 'utime': row[6], 207 | 'ctime': row[7], 208 | }) 209 | return res 210 | 211 | return run_sql(cb) 212 | 213 | 214 | def list_config_kv(enable: bool = None, like_key: str = None) -> dict: 215 | """ 216 | 列出所有配置信息 217 | :return: {k:v,} 218 | """ 219 | config = list_config(enable, like_key) 220 | res = {} 221 | for key in config: 222 | vo = config.get(key) 223 | res[key] = vo.value 224 | 225 | return kv2dict(res) 226 | 227 | 228 | def list_config_ko(enable: bool = None, like_key: str = None) -> dict: 229 | """ 230 | 列出所有配置信息 231 | :return: {k:ConfigVO,} 232 | """ 233 | config = list_config(enable, like_key) 234 | return kv2dict(config) 235 | 236 | 237 | def update_config(data: List[ConfigVO], merge: bool = False) -> int: 238 | """ 239 | 更新配置信息 240 | :rtype: object 241 | :param data: [{key,value,enable},{key,value}...] 242 | :param merge: 是否为 merge 模式:delete -> insert 243 | :return: 生效数目 244 | """ 245 | 246 | def cb(cur: Cursor): 247 | res = 0 248 | for vo in data: 249 | key = vo.key 250 | value = vo.value 251 | pytype = type(value).__name__ 252 | enable = vo.enable 253 | 254 | update_sql = "update config set key = key {} {} {} where key = '{}';".format( 255 | ",value = '{}'".format( 256 | __str_escape(value) if __is_direct_type(pytype) else __str_escape(utils.serialize(value))), 257 | ",pytype = '{}'".format(pytype), 258 | ",enable = {}".format(1 if enable else 0) if enable is not None else '', 259 | __str_escape(key) 260 | ) 261 | cur.execute(update_sql) 262 | res += cur.rowcount 263 | 264 | if merge: 265 | merge_sql = "insert into config(key, value, pytype, enable) select '{}', '{}', '{}', {} where (select changes() = 0);".format( 266 | __str_escape(key), 267 | __str_escape(value) if value is not None else '', 268 | pytype if value is not None else '', 269 | (1 if enable else 0) if enable is not None else 1, 270 | ) 271 | cur.execute(merge_sql) 272 | res += cur.rowcount 273 | return res 274 | 275 | return run_sql(cb) 276 | 277 | 278 | def get_config(key: str, enable: bool = None) -> ConfigVO: 279 | """ 280 | 获取单个配置项 281 | :param key: key 282 | :param enable: 是否有效 283 | :return: 配置项值 284 | """ 285 | 286 | def cb(cur: Cursor): 287 | sql = "select value, pytype, defaults, enable, comment, utime, ctime " \ 288 | "from config where key = '{}' {};".format( 289 | key, 290 | '' if enable is None else 'and enable = {}'.format(1 if enable else 0) 291 | ) 292 | cur.execute(sql) 293 | res = cur.fetchone() 294 | 295 | return __get_result({ 296 | 'key': key, 297 | 'value': res[0], 298 | 'pytype': res[1], 299 | 'defaults': res[2], 300 | 'enable': res[3], 301 | 'comment': res[4], 302 | 'utime': res[5], 303 | 'ctime': res[6], 304 | }) if res else None 305 | 306 | return run_sql(cb) 307 | 308 | 309 | def delete_config(key: Union[List[str], str]) -> int: 310 | """ 311 | 删除配置项目 312 | :param key: key列表,或字符串(使用like,如'key%') 313 | :return: 生效数目 314 | """ 315 | 316 | def cb(cur: Cursor): 317 | if isinstance(key, list): 318 | incondition = '' 319 | for k in key: 320 | incondition += "'{}',".format(k) 321 | if incondition: 322 | incondition = incondition.rstrip(',') 323 | sql = "delete from config where key in ({});".format(incondition) 324 | cur.execute(sql) 325 | elif isinstance(key, str): 326 | sql = "delete from config where key like '{}';".format(key) 327 | cur.execute(sql) 328 | return cur.rowcount 329 | 330 | return run_sql(cb) 331 | 332 | 333 | def add_config(data: List[ConfigVO]) -> int: 334 | """ 335 | 添加配置项 336 | :param data: [{key,value}...] 337 | :return: 生效数目 338 | """ 339 | 340 | def cb(cur: Cursor): 341 | res = 0 342 | for vo in data: 343 | value = vo.value 344 | pytype = type(value).__name__ 345 | defaults = vo.defaults 346 | enable = vo.enable 347 | comment = vo.comment 348 | 349 | sql = "insert into config(key, value, pytype, defaults, enable, comment) values('{}', '{}', '{}', '{}', {}, '{}')".format( 350 | __str_escape(vo.key), 351 | __str_escape(value) if __is_direct_type(pytype) else __str_escape(utils.serialize(value)), 352 | pytype, 353 | __str_escape(utils.serialize(defaults)), 354 | 1 if enable is None else (1 if enable else 0), 355 | __str_escape(comment) if comment is not None else '', 356 | ) 357 | cur.execute(sql) 358 | res += cur.rowcount 359 | return res 360 | 361 | return run_sql(cb) 362 | -------------------------------------------------------------------------------- /src/py/get_background.py: -------------------------------------------------------------------------------- 1 | # -*-coding:utf-8-*- 2 | import imghdr 3 | import json 4 | import os 5 | import random 6 | import shutil 7 | import threading 8 | import time 9 | from concurrent.futures import ThreadPoolExecutor 10 | from concurrent.futures._base import Future 11 | from typing import Dict, List 12 | 13 | import requests 14 | 15 | import configurator as configr 16 | import const_config as const 17 | import utils 18 | from component import CustomLogger, SingletonMetaclass 19 | from vo import ConfigVO, E 20 | 21 | log = CustomLogger().get_logger() 22 | 23 | 24 | class GetBackgroundTask(object, metaclass=SingletonMetaclass): 25 | from set_background import SetBackgroundTask 26 | 27 | """ 28 | 获取随机壁纸 29 | 30 | @author Myles Yang 31 | """ 32 | 33 | def __init__(self, config: Dict[str, ConfigVO]): 34 | # 下载的壁纸任务 35 | # 总任务数量 36 | self.taskCount: int = 0 37 | # 已完成的任务数量(不论成功失败) 38 | self.taskDoneCount: int = 0 39 | # 已完成且成功的任务数量 40 | self.taskDoneSucceedCount: int = 0 41 | 42 | # 下载任务开始前,保存旧数据,任务完成后删除 43 | self.old_bg_abspaths: List[str] = [] 44 | 45 | # 本次拉取下载的图片URL 46 | self.await_dwn_bg_urls: List[str] = [] 47 | 48 | # 任务模式为一张时使用,记录当前已拉取到的下标,self.get_random_bg_urls()[self.single_curidx] 49 | self.single_curidx: int = 0 50 | # 任务模式:一张,是否在下载中 51 | self.single_dwning: bool = False 52 | 53 | self.is_getting_bg_urls: bool = False 54 | 55 | self.config: Dict[str, ConfigVO] = config 56 | self.sb_task: GetBackgroundTask.SetBackgroundTask = None 57 | 58 | def init_task(self, task: SetBackgroundTask): 59 | """ 60 | 初始化设置壁纸任务对象 61 | """ 62 | self.sb_task = task 63 | 64 | def run(self): 65 | """ 66 | 拉取壁纸 67 | :return 68 | """ 69 | if not self.sb_task: 70 | raise AttributeError("SetBackgroundTask对象未初始化,请执行init_task方法") 71 | 72 | if self.is_getting_bg_urls: 73 | return 74 | 75 | task_mode = configr.get_task_mode(self.config) 76 | if task_mode == const.Key.Task._MODE_MULTIPLE.value and self.taskCount > 0 \ 77 | or task_mode == const.Key.Task._MODE_SINGLE.value and self.single_dwning: 78 | # log.info('后台正在拉取壁纸...请等待任务完成在执行此操作!') 79 | # utils.create_dialog('后台正在拉取壁纸...\n\n请等待任务完成在执行此操作!', const.dialog_title, 80 | # interval=2, style=win32con.MB_ICONWARNING) 81 | return 82 | # 保存原来的壁纸的路径 83 | self.old_bg_abspaths = configr.get_bg_abspaths() 84 | # 拉取壁纸 85 | self.auto_dwn_bg() 86 | 87 | def get_random_bg_urls(self) -> List[str]: 88 | """ 根据图源类型获取对应随机壁纸链接列表 """ 89 | proxies = configr.get_proxies() 90 | if proxies: 91 | log.info('使用代理服务器配置拉取壁纸: {}'.format(proxies)) 92 | api_name = configr.get_api_name(self.config) 93 | if api_name == const.Key.Api._NAME_WALLHAVEN.value: 94 | return self.__get_wallhaven_bg_urls(proxies) 95 | return self.__get_custom_bg_urls(proxies) 96 | 97 | def __get_wallhaven_bg_urls(self, proxies=None) -> List[str]: 98 | """ 获取wallhaven随机壁纸链接列表 """ 99 | params = utils.get_request_params(configr.get_wallhaven_url(self.config)) 100 | apikey = configr.get_wallhaven_apikey(self.config) 101 | if apikey: 102 | params['apikey'] = apikey 103 | if params.get('sorting') is None: params['sorting'] = 'random' 104 | bg_url_list = [] 105 | resp = None 106 | try: 107 | # 如果排序未非随机,则附带随机页码参数 108 | if params.get('sorting') != 'random': 109 | params['page'] = 1 110 | resp = requests.get(const.wallhaven_api, params, headers=const.headers, proxies=proxies, 111 | timeout=(5, 60)) 112 | if resp and resp.status_code == 200: 113 | try: 114 | data = json.loads(resp.text) 115 | last_page = data.get('meta').get('last_page') 116 | params['page'] = random.randint(1, int(last_page)) 117 | except: 118 | params['page'] = 1 119 | time.sleep(1) 120 | resp = requests.get(const.wallhaven_api, params, headers=const.headers, proxies=proxies, timeout=(5, 60)) 121 | except Exception as e: 122 | log.error('获取Wallhaven壁纸链接列表超时: {}'.format(e)) 123 | 124 | if resp and resp.status_code == 200: 125 | try: 126 | data = json.loads(resp.text) 127 | for item in data.get('data'): 128 | bg_url = item.get('path') 129 | if bg_url: 130 | bg_url_list.append(bg_url) 131 | except Exception as e: 132 | log.error('Wallhaven JSON数据解析错误: {}'.format(e)) 133 | 134 | if resp and resp.status_code != 200: 135 | log.error("获取Wallhaven壁纸链接列表失败,状态码: {},URL: {},错误: {}".format(resp.status_code, resp.url, resp.text)) 136 | 137 | return bg_url_list 138 | 139 | def __get_custom_bg_urls(self, proxies=None) -> List[str]: 140 | """ 获取自定义图源随机壁纸链接列表 """ 141 | num = 24 # 链接数量,与wallhaven保持一致 142 | bg_url_list = [] 143 | custom_urls = configr.get_custom_urls(self.config) 144 | if custom_urls: 145 | while num > 0: 146 | idx = random.randint(0, len(custom_urls) - 1) 147 | bg_url_list.append(custom_urls[idx]) 148 | num -= 1 149 | return bg_url_list 150 | return self.__get_wallhaven_bg_urls(proxies) 151 | 152 | def auto_retain_bgs(self): 153 | bg_srcpath = configr.get_wallpaper_abspath(self.config) 154 | 155 | is_retain_bgs = configr.is_retain_bg_files(self.config) 156 | max_retain_mb = configr.get_max_retain_bg_mb(self.config) 157 | if is_retain_bgs and max_retain_mb != 0: 158 | # 保留壁纸 159 | dirname = time.strftime("retain-%Y%m%d", time.localtime()) 160 | dir_path = os.path.join(bg_srcpath, dirname) 161 | os.makedirs(dir_path, exist_ok=True) 162 | for path in self.old_bg_abspaths: 163 | if os.path.isfile(path): 164 | try: 165 | shutil.move(path, dir_path) 166 | except: 167 | pass 168 | # 超出最大占用空间的进行删除 169 | if max_retain_mb != -1: # -1无限制 170 | allow_size = max_retain_mb * 1024 * 1024 171 | # 1、获取已经保存的总大小 172 | bg_path_size = utils.get_path_size(bg_srcpath) 173 | # 2、进行删除,删除时间旧的(简单粗暴:直接删除一天) 174 | cur_size = bg_path_size 175 | if bg_path_size > allow_size: 176 | # 获取已保存文件夹 177 | dirs = list(filter(lambda p: os.path.isdir(os.path.join(bg_srcpath, p)), os.listdir(bg_srcpath))) 178 | dirs.sort() 179 | for p in dirs: 180 | path = os.path.join(bg_srcpath, p) 181 | size = utils.get_path_size(path) 182 | succeed = utils.rm_file_dir(path) 183 | if succeed: 184 | cur_size -= size 185 | if cur_size <= allow_size: 186 | break 187 | else: # 删除 188 | for path in self.old_bg_abspaths: 189 | if os.path.isfile(path): 190 | try: 191 | os.remove(path) 192 | except: 193 | pass 194 | self.old_bg_abspaths = [] 195 | 196 | def __dwn_bg(self, bg_url: str, filename: str = None) -> str: 197 | """ 198 | 下载单个壁纸到 “const.bg_srcpath” 目录中 199 | :param bg_url: 壁纸链接 200 | :param filename: 壁纸文件名 201 | :return: 下载成功返回壁纸保存的绝对位置,否则为None 202 | """ 203 | res_bg_abspath = None 204 | try: 205 | resp = requests.get(bg_url, headers=const.headers, proxies=configr.get_proxies(), timeout=(5, 120)) 206 | if resp.status_code == 200: 207 | bg_srcpath = configr.get_wallpaper_abspath(self.config) 208 | os.makedirs(bg_srcpath, exist_ok=True) 209 | filename = filename if filename else "{}-{}".format(int(time.time() * 1000), os.path.basename(bg_url)) 210 | bg_abspath = os.path.abspath(os.path.join(bg_srcpath, utils.get_win_valid_filename(filename))) 211 | with open(bg_abspath, 'wb') as bgfile: 212 | bgfile.write(resp.content) 213 | res_bg_abspath = bg_abspath 214 | else: 215 | log.error("下载壁纸失败,状态码: {},URL: {},错误:\n {}".format(resp.status_code, resp.url, resp.text)) 216 | except Exception as e: 217 | exc = E().e(e) 218 | log.error("下载壁纸失败: 原因:{},URL:{},错误:\n{}".format(exc.message, bg_url, exc.cause)) 219 | return res_bg_abspath 220 | 221 | # 检测壁纸是否可用 222 | if utils.is_background_valid(res_bg_abspath): 223 | log.info('壁纸[{}]下载成功,URL:{}'.format(filename, bg_url)) 224 | # 补全后缀 225 | fn, fe = os.path.splitext(filename) 226 | if not fe: 227 | try: 228 | ext = imghdr.what(res_bg_abspath) 229 | if ext: 230 | new_res_bg_abspath = res_bg_abspath + '.' + ext 231 | shutil.move(res_bg_abspath, new_res_bg_abspath) 232 | res_bg_abspath = new_res_bg_abspath 233 | except: 234 | pass 235 | else: 236 | try: 237 | os.remove(res_bg_abspath) 238 | except: 239 | pass 240 | res_bg_abspath = None 241 | 242 | return res_bg_abspath 243 | 244 | def __single_dwn_bg(self, bg_url): 245 | """ 单线程下载壁纸:(任务模式:一张) """ 246 | self.single_dwning = True 247 | try: 248 | bg_abspath = self.__dwn_bg(bg_url) 249 | if bg_abspath: 250 | self.sb_task.set_bg_paths([]) 251 | self.sb_task.add_bg_path(bg_abspath) 252 | self.sb_task.set_background_idx(0) 253 | self.auto_retain_bgs() 254 | finally: 255 | self.single_curidx += 1 256 | self.single_dwning = False 257 | 258 | def __parallel_dwn_bg(self, bg_urls): 259 | """ 260 | 多线程下载壁纸:(任务模式:多张) 261 | 对于限流的网站,降低线程数,增加下载图片前的延时,以提高下载成功率 262 | :param bg_urls: 壁纸链接列表 263 | """ 264 | self.taskCount = len(bg_urls) 265 | 266 | def dwn(bg_url: str, filename: str = None) -> str: 267 | rndsleep = configr.get_random_sleep(self.config) 268 | time.sleep(random.uniform(rndsleep[0], rndsleep[1])) 269 | return self.__dwn_bg(bg_url, filename) 270 | 271 | log.info('正在使用多线程拉取新的随机壁纸...') 272 | log.info('线程数量: {},下载每个壁纸前随机暂停时间在{}之间'.format( 273 | configr.get_dwn_threads(self.config), 274 | configr.get_random_sleep(self.config) 275 | )) 276 | 277 | threads = configr.get_dwn_threads(self.config) 278 | with ThreadPoolExecutor(max_workers=threads, thread_name_prefix='DwnBg') as pool: 279 | for bg_url in bg_urls: 280 | future = pool.submit(dwn, bg_url) 281 | future.add_done_callback(self.__bg_parallel_dwned_callback) 282 | pool.shutdown(wait=True) 283 | 284 | def __bg_parallel_dwned_callback(self, future: Future): 285 | """ 286 | 重要函数,图片多线程下载完毕时的回调函数 287 | :param future: concurrent.futures._base.Future() 288 | """ 289 | self.taskDoneCount += 1 290 | 291 | bg_abspath = str(future.result()) 292 | if bg_abspath != str(None): 293 | self.taskDoneSucceedCount += 1 294 | self.sb_task.add_bg_path(bg_abspath) 295 | # 下载完成一张图片马上更新桌面背景 296 | if self.taskDoneSucceedCount == 1: 297 | self.sb_task.set_bg_paths([]) 298 | self.sb_task.add_bg_path(bg_abspath) 299 | self.sb_task.set_background_idx(0) 300 | 301 | # 任务完成 302 | if self.taskDoneCount >= self.taskCount: 303 | log.info('壁纸拉取完毕,任务总数{},成功{}'.format(self.taskCount, self.taskDoneSucceedCount)) 304 | if self.taskDoneSucceedCount > 0: 305 | self.auto_retain_bgs() 306 | # 重置任务计数 307 | self.taskCount = self.taskDoneCount = self.taskDoneSucceedCount = 0 308 | 309 | def auto_dwn_bg(self): 310 | """ 拉取新壁纸,任务模式:一张 / 多张 """ 311 | if self.is_getting_bg_urls: return 312 | self.is_getting_bg_urls = True 313 | try: 314 | task_mode = configr.get_task_mode(self.config) 315 | if task_mode == const.Key.Task._MODE_SINGLE.value: # 每次下载一张 316 | if self.single_curidx >= len(self.await_dwn_bg_urls) - 1: 317 | self.await_dwn_bg_urls = self.get_random_bg_urls() 318 | self.single_curidx = 0 319 | if self.await_dwn_bg_urls: 320 | threading.Thread( 321 | target=lambda: self.__single_dwn_bg(self.await_dwn_bg_urls[self.single_curidx])).start() 322 | else: # 每次下载多张 323 | self.await_dwn_bg_urls = self.get_random_bg_urls() 324 | if self.await_dwn_bg_urls: 325 | threading.Thread(target=lambda: self.__parallel_dwn_bg(self.await_dwn_bg_urls)).start() 326 | else: 327 | self.taskCount = 0 328 | finally: 329 | self.is_getting_bg_urls = False 330 | -------------------------------------------------------------------------------- /src/py/service.py: -------------------------------------------------------------------------------- 1 | # -*-coding:utf-8-*- 2 | 3 | """ 4 | 业务逻辑 5 | 6 | @author Myles Yang 7 | """ 8 | import os 9 | import time 10 | from typing import List 11 | 12 | import application as app 13 | import args_definition as argsdef 14 | import configurator as configr 15 | import const_config as const 16 | import dao 17 | import utils 18 | from vo import E, ConfigVO, StatusVO 19 | 20 | 21 | def get_status(): 22 | """ 获取程序状态 """ 23 | status = StatusVO() 24 | 25 | # 运行状态 26 | bpid = configr.get_bpid() 27 | status.running = bpid and utils.is_process_running(bpid, app.app_fullpath) 28 | 29 | return status 30 | 31 | 32 | def toggle_startup_shutdown(): 33 | """ 切换程序开关 """ 34 | 35 | def check_running_status(running: bool): 36 | loop = 60 # 30 seconds allowable check time 37 | while loop > 0: 38 | bpid = configr.get_bpid() 39 | if running: 40 | if bpid and utils.is_process_running(bpid, app.app_fullpath): 41 | break 42 | else: 43 | if bpid and not utils.is_process_running(bpid, app.app_fullpath): 44 | break 45 | time.sleep(0.5) 46 | loop -= 1 47 | 48 | bpid = configr.get_bpid() 49 | if bpid and utils.is_process_running(bpid, app.app_fullpath): 50 | configr.record_bpid(False) 51 | utils.kill_process([bpid]) 52 | check_running_status(False) 53 | else: 54 | args = ' {} {} {} {}'.format(argsdef.ARG_RUN, argsdef.ARG_RUN_TYPE_BACKGROUND, 55 | argsdef.ARG_LOG, argsdef.ARG_LOG_TYPE_FILE) 56 | succeed, msg = utils.run_in_background(app.app_fullpath, args) 57 | if succeed: 58 | check_running_status(True) 59 | return get_status() 60 | 61 | 62 | def update_config(config: dict): 63 | """ 更新配置信息(暂不做参数检验) """ 64 | try: 65 | config = dao.dict2kv(config) 66 | update_list: List[ConfigVO] = [] 67 | for k in config: 68 | update_list.append(ConfigVO(k, config.get(k))) 69 | 70 | # 自定义图源,先删除再添加 71 | api_name = config.get(const.Key.Api.NAME.value) 72 | if api_name == const.Key.Api._NAME_CUSTOM.value: 73 | dao.delete_config('{}%'.format(const.Key.Api.CUSTOM.value)) 74 | 75 | ures = dao.update_config(update_list, merge=True) 76 | if ures > 0: 77 | # 开机启动 78 | run_startup = config.get(const.Key.Run.STARTUP.value) 79 | if run_startup is not None: 80 | create_startup_lnk() if run_startup else delete_startup_lnk() 81 | # 桌面右键菜单 82 | create_desktop_context_menu(dao.list_config()) 83 | 84 | except Exception as e: 85 | raise E().e(e, message="配置更新失败") 86 | 87 | 88 | def create_startup_lnk(log_type: str = None): 89 | """ 90 | 创建快捷方式到用户开机启动目录: shell:startup 91 | """ 92 | args = '{} {} {} {}'.format(argsdef.ARG_RUN, argsdef.ARG_RUN_TYPE_BACKGROUND, 93 | argsdef.ARG_LOG, log_type if log_type else argsdef.ARG_LOG_TYPE_FILE) 94 | return utils.create_shortcut(app.app_fullpath, 'shell:startup:{}'.format(const.app_name), args=args, style=7) 95 | 96 | 97 | def delete_startup_lnk(): 98 | """ 删除开启自启快捷方式 """ 99 | try: 100 | startup_path = utils.get_special_folders('startup') 101 | paths = os.listdir(startup_path) 102 | for path in paths: 103 | name, ext = os.path.splitext(path) 104 | if '.lnk' == ext.lower(): 105 | lnkpath = os.path.join(startup_path, path) 106 | target_lnk_path = utils.get_shortcut(lnkpath) 107 | if os.path.normcase(target_lnk_path) == os.path.normcase(app.app_fullpath): 108 | os.remove(lnkpath) 109 | except Exception as e: 110 | raise E().e(e, message="取消开机自启错误") 111 | 112 | 113 | def locate_favorite_path(): 114 | """ 打开收藏文件夹 """ 115 | favorite_path = configr.get_favorite_abspath() 116 | 117 | if os.path.isdir(favorite_path): 118 | utils.locate_path(favorite_path, False) 119 | else: 120 | raise E().ret(message="收藏目录不存在:{}".format(favorite_path)) 121 | 122 | 123 | def locate_workdir(): 124 | """ 打开工作目录 """ 125 | workdir = configr.get_workdir() 126 | workdir = workdir if workdir else 'run' 127 | 128 | if os.path.isdir(workdir): 129 | utils.locate_path(workdir, False) 130 | else: 131 | raise E().ret(message="工作目录不存在:{}".format(workdir)) 132 | 133 | 134 | def create_desktop_context_menu(config=None): 135 | """ 删除 -> 添加 """ 136 | utils.delete_desktop_context_menu(const.app_name_en) 137 | if not configr.is_ctxmenu_enabled(config): return 138 | 139 | prev = configr.is_ctxmenu_prev_enabled(config) 140 | next = configr.is_ctxmenu_next_enabled(config) 141 | fav = configr.is_ctxmenu_favorite_enabled(config) 142 | loc = configr.is_ctxmenu_locate_enabled(config) 143 | 144 | if not (prev or next or fav or loc): return 145 | 146 | def get_sub_menu(name, cmd, icon_idx, order): 147 | return [ 148 | { 149 | 'sub_key': '{}\\shell\\itm{}-{}'.format(const.app_name_en, order, cmd), 150 | 'values': { 151 | 'Icon': '{},{}'.format(app.app_fullpath, icon_idx), 152 | 'MUIVerb': name, 153 | } 154 | }, 155 | { 156 | 'sub_key': '{}\\shell\\itm{}-{}\\command'.format(const.app_name_en, order, cmd), 157 | 'values': { 158 | '': '{} --run cmd --cmd {}'.format(app.app_fullpath, cmd), 159 | } 160 | } 161 | ] 162 | 163 | reg_to_create = [] 164 | if prev: 165 | reg_to_create.append(('上一张壁纸', 'pre', 1, 1)) 166 | if next: 167 | reg_to_create.append(('下一张壁纸', 'nxt', 2, 2)) 168 | if fav: 169 | reg_to_create.append(('收藏当前壁纸', 'fav', 3, 3)) 170 | if loc: 171 | reg_to_create.append(('定位当前壁纸', 'loc', 4, 4)) 172 | 173 | if len(reg_to_create) == 1: 174 | data = reg_to_create[0] 175 | utils.create_desktop_context_menu(const.app_name_en, { 176 | 'Icon': '{},{}'.format(app.app_fullpath, 0), 177 | 'MUIVerb': data[0], 178 | }) 179 | utils.create_desktop_context_menu(const.app_name_en + "\\command", { 180 | '': '{} --run cmd --cmd {}'.format(app.app_fullpath, data[1]), 181 | }) 182 | else: 183 | utils.create_desktop_context_menu(const.app_name_en, { 184 | 'Icon': '{},{}'.format(app.app_fullpath, 0), 185 | 'MUIVerb': const.app_name, 186 | 'SubCommands': '' 187 | }) 188 | for tp in reg_to_create: 189 | for d in get_sub_menu(*tp): 190 | utils.create_desktop_context_menu(d['sub_key'], d['values']) 191 | -------------------------------------------------------------------------------- /src/py/set_background.py: -------------------------------------------------------------------------------- 1 | # -*-coding:utf-8-*- 2 | import ctypes 3 | import os 4 | import random 5 | import shutil 6 | import time 7 | from threading import Thread 8 | from typing import Dict, Callable, List 9 | 10 | import win32con 11 | import win32gui 12 | from system_hotkey import SystemHotkey 13 | 14 | import args_definition as argsdef 15 | import configurator as configr 16 | import const_config as const 17 | import utils 18 | from component import CustomLogger, SingletonMetaclass, SimpleTaskTimer, SimpleMmapActuator 19 | from utils import is_background_valid 20 | from vo import ConfigVO, E 21 | 22 | user32 = ctypes.windll.user32 23 | 24 | log = CustomLogger().get_logger() 25 | 26 | # 允许的使用快捷键时进行切换的间隔时间 27 | _allowed_manual_switching_interval_time = 0.562632 28 | 29 | 30 | class SetBackgroundTask(object, metaclass=SingletonMetaclass): 31 | """ 32 | 切换桌面背景后台任务 33 | 34 | @author Myles Yang 35 | """ 36 | 37 | def __init__(self, config: Dict[str, ConfigVO], get_bg_func: Callable): 38 | """ 39 | :param config: 配置 40 | :param get_bg_func: 拉取壁纸的函数 41 | """ 42 | self.__config = config 43 | self.__bg_paths = configr.get_bg_abspaths() 44 | self.__current = self.set_current(configr.get_current(config)) 45 | self.__seconds = configr.get_seconds(config) 46 | 47 | self.__func_get_bg = get_bg_func 48 | if not (get_bg_func or callable(get_bg_func)): 49 | raise AttributeError("请正确传入拉取壁纸的函数形参get_bg_func") 50 | 51 | # 记录上一次切换壁纸的时间,防止频繁切换 52 | self.__last_change_time = time.time() - 1314.71861 53 | 54 | self.__timer: SimpleTaskTimer = None 55 | self.__hotkey: SystemHotkey = None 56 | 57 | def get__bg_paths(self): 58 | """ 59 | bg_paths getter 60 | """ 61 | return self.__bg_paths 62 | 63 | def set_bg_paths(self, bg_paths: list): 64 | """ 65 | bg_paths setter 66 | """ 67 | self.__bg_paths = bg_paths 68 | 69 | def add_bg_path(self, bg_path: str): 70 | """ 71 | add to bg_paths 72 | :param bg_path: 壁纸绝对路径 73 | """ 74 | self.__bg_paths.append(bg_path) 75 | 76 | def get_current(self): 77 | """ 78 | current getter 79 | """ 80 | return self.__current 81 | 82 | def set_current(self, current: int): 83 | """ 84 | current setter 85 | """ 86 | current = current if 0 <= current < len(self.__bg_paths) else 0 87 | self.__current = current 88 | configr.set_current(current) 89 | return current 90 | 91 | def run(self): 92 | """ 93 | 启动切换桌面背景后台任务 94 | """ 95 | if self.__timer and self.__timer.is_running(): 96 | log.info('自动切换随机桌面背景后台任务已启动,无需再启动') 97 | return 98 | 99 | log.info('自动切换随机桌面背景任务已启动') 100 | log.info('桌面背景切换间隔:{}秒'.format(self.__seconds)) 101 | 102 | # 绑定全局热键 103 | self.bind_hotkey() 104 | # 监听执行命令 105 | self.monitor_command() 106 | 107 | # 开启后台定时切换任务 108 | self.__timer = SimpleTaskTimer() 109 | self.__timer.run(self.__seconds, self.next_bg) 110 | 111 | # 切换当前壁纸 112 | if self.__bg_paths: 113 | if 0 <= self.__current < len(self.__bg_paths): 114 | self.set_background(self.__bg_paths[self.__current]) 115 | else: 116 | self.set_background_idx(0) 117 | else: 118 | self.set_current(0) 119 | self.next_bg() 120 | 121 | def next_bg(self): 122 | """ 123 | 切换下一个壁纸 124 | """ 125 | 126 | # 锁屏状态下不切换 127 | if utils.is_lock_workstation(): 128 | return 129 | 130 | if configr.is_local_disorder(self.__config): 131 | self.random_bg() 132 | return 133 | 134 | # 限制热键切换频率 135 | if time.time() - self.__last_change_time < _allowed_manual_switching_interval_time: 136 | return 137 | 138 | # 重新拉取图片 139 | if (not self.__bg_paths) or self.__current >= len(self.__bg_paths) - 1: 140 | self.__get_backgrounds() 141 | return 142 | nxt = self.__current + 1 143 | while nxt < len(self.__bg_paths): 144 | set_res = self.set_background(self.__bg_paths[nxt], nxt) 145 | if set_res: 146 | self.set_current(nxt) 147 | return 148 | nxt += 1 149 | if nxt >= len(self.__bg_paths) - 1: 150 | self.__get_backgrounds() 151 | 152 | def prev_bg(self): 153 | """ 154 | 切换上一个壁纸 155 | """ 156 | if configr.is_local_disorder(self.__config): 157 | self.random_bg() 158 | return 159 | 160 | if time.time() - self.__last_change_time < _allowed_manual_switching_interval_time: 161 | return 162 | 163 | if (not self.__bg_paths) or self.__current <= 0: 164 | utils.create_dialog("已是第一个桌面背景了,不能再切换上一个", const.dialog_title, 165 | interval=2, style=win32con.MB_ICONWARNING) 166 | return 167 | pre = self.__current - 1 168 | while pre >= 0: 169 | set_res = self.set_background(self.__bg_paths[pre], pre) 170 | if set_res: 171 | self.set_current(pre) 172 | return 173 | pre -= 1 174 | 175 | def random_bg(self): 176 | """ 随机切换壁纸 """ 177 | if not self.__bg_paths: 178 | return 179 | idx = random.randint(0, len(self.__bg_paths) - 1) 180 | self.set_background_idx(idx) 181 | 182 | def locate_bg(self): 183 | """ 184 | 在资源管理器定位当前桌面背景文件 185 | """ 186 | if self.__bg_paths: 187 | utils.locate_path(self.__bg_paths[self.__current]) 188 | 189 | def favorite_bg(self): 190 | """ 191 | 收藏当前壁纸至收藏目录 192 | """ 193 | if self.__bg_paths: 194 | favorite_srcpath = configr.get_favorite_abspath(self.__config) 195 | os.makedirs(favorite_srcpath, exist_ok=True) 196 | try: 197 | shutil.copy(self.__bg_paths[self.__current], favorite_srcpath) 198 | utils.create_dialog('已收藏壁纸[{}]至收藏目录'.format(os.path.basename(self.__bg_paths[self.__current])), 199 | const.dialog_title, style=win32con.MB_ICONINFORMATION, interval=2) 200 | except: 201 | pass 202 | 203 | def __get_backgrounds(self): 204 | """ 205 | 重新拉取壁纸 206 | """ 207 | if configr.get_rotation(self.__config) == const.Key.Run._ROTATION_LOCAL.value: 208 | """ 本地顺序轮询 """ 209 | self.set_background_idx(0) 210 | return 211 | # if not utils.is_network_available(): 212 | # log.info('网络不连通,取消拉取壁纸,重新轮换已下载的壁纸') 213 | # self.set_background_idx(0) 214 | # return 215 | # self.__func_get_bg() 216 | Thread(target=self.__func_get_bg).start() 217 | 218 | def set_background(self, abs_path: str, index: int = None): 219 | """ 220 | 设置桌面背景,使用windows api 221 | 222 | 如果路径无效,也会生成一张纯色的图片,设置的壁纸会临时存放在路径 223 | "%USERPROFILE%/AppData/Roaming/Microsoft/Windows/Themes/CachedFiles" 224 | 225 | :param abs_path: 壁纸绝对路径 226 | :param index: 当前壁纸列表下标 227 | :return: True-设置成功; False-设置失败 228 | """ 229 | if is_background_valid(abs_path): 230 | try: 231 | # 在32位win7中测试,频繁切换会报错,故限制了切换频率 232 | # 设置桌面背景,最后一个参数:SPIF_UPDATEINIFILE(在原路径设置),win32con.SPIF_SENDWININICHANGE(复制图片到缓存) 233 | win32gui.SystemParametersInfo(win32con.SPI_SETDESKWALLPAPER, abs_path, win32con.SPIF_SENDWININICHANGE) 234 | self.__last_change_time = time.time() 235 | except: 236 | return False 237 | cur_bg = index + 1 if (index or index == 0) else self.__current + 1 238 | log.info('切换桌面背景,总数{},当前{}'.format(len(self.__bg_paths), cur_bg)) 239 | return True 240 | return False 241 | 242 | def set_background_idx(self, index: int): 243 | """ 设置壁纸 """ 244 | if self.__bg_paths and 0 <= index < len(self.__bg_paths): 245 | s = self.set_background(self.__bg_paths[index]) 246 | if s: self.set_current(index) 247 | return s 248 | return False 249 | 250 | def __get_hotkey_cb(self, data: dict): 251 | def callback(): 252 | try: 253 | log.info('执行热键[{}]:{}'.format(data.get('name'), data.get('hk'))) 254 | data.get('cb')() 255 | except Exception as e: 256 | exc = E().e(e) 257 | log.error('热键[{}]事件[{}]执行错误:{},原因:\n{}'.format( 258 | data.get('hk'), data.get('name'), exc.message, exc.cause)) 259 | 260 | return lambda p: callback() 261 | 262 | def bind_hotkey(self): 263 | """ 264 | 绑定热键 265 | """ 266 | if self.__hotkey or not configr.is_hotkey_enabled(self.__config): 267 | return 268 | 269 | self.__hotkey = SystemHotkey(check_queue_interval=0.125) 270 | # 热键绑定的数量 271 | bind_count = 0 272 | # 热键绑定成功的数量 273 | bind_succeed_count = 0 274 | # 热键绑定失败提示消息 275 | bind_error_msg = '' 276 | 277 | hks_to_bind = [ 278 | { 279 | 'hk': configr.get_hotkey_prev(self.__config), 280 | 'name': '上一张桌面壁纸', 281 | 'cb': self.prev_bg 282 | }, 283 | { 284 | 'hk': configr.get_hotkey_next(self.__config), 285 | 'name': '下一张桌面壁纸', 286 | 'cb': self.next_bg 287 | }, 288 | { 289 | 'hk': configr.get_hotkey_locate(self.__config), 290 | 'name': '定位当前桌面壁纸文件', 291 | 'cb': self.locate_bg 292 | }, 293 | { 294 | 'hk': configr.get_hotkey_favorite(self.__config), 295 | 'name': '收藏当前桌面壁纸', 296 | 'cb': self.favorite_bg 297 | } 298 | ] 299 | 300 | for hk_info in hks_to_bind: 301 | hk = hk_info.get('hk') 302 | hk_name = hk_info.get('name') 303 | if hk: 304 | bind_count += 1 305 | valid, msg = self.is_hotkey_valid(self.__hotkey, hk) 306 | if valid: 307 | # system_hotkey 模块存在Bug,作者尚未修复 308 | # 热键在子线程中注册,其中overwrite无论真假,当试图绑定了一个原有的热键时,会抛出错误, 309 | # 但无法在主线程捕获,而且会造成绑定热键的队列阻塞?,进而造成所有热键失效。 310 | try: 311 | self.__hotkey.register(hk, callback=self.__get_hotkey_cb(hk_info), overwrite=True) 312 | except Exception as e: 313 | log.error('热键[{}]绑定失败,热键:{},原因:{}'.format(hk_name, hk, e)) 314 | bind_error_msg += '热键[{}]绑定失败:{}\n'.format(hk_name, hk) 315 | continue 316 | bind_succeed_count += 1 317 | log.info('热键[{}]绑定成功: {}'.format(hk_name, hk)) 318 | else: 319 | log.error('热键[{}]绑定失败,热键:{},原因:{}'.format(hk_name, hk, msg)) 320 | bind_error_msg += '热键[{}]绑定失败:{}\n'.format(hk_name, hk) 321 | 322 | # 检测热键绑定情况 323 | if bind_succeed_count == 0: 324 | self.__hotkey = None 325 | elif bind_succeed_count < bind_count: 326 | # 设置非阻塞自动关闭对话框,防止阻塞线程 327 | utils.create_dialog(bind_error_msg, const.dialog_title, style=win32con.MB_ICONWARNING, interval=7) 328 | 329 | def unbind_hotkey(self): 330 | """ 331 | 解绑热键 332 | """ 333 | pass 334 | 335 | def is_hotkey_valid(self, hkobj: SystemHotkey, hk: List[str]): 336 | """ 337 | 检测热键是否可用,因为 SystemHotkey 注册热键存在BUG,在注册前进行检测? 338 | """ 339 | hk = hkobj.order_hotkey(hk) 340 | try: 341 | keycode, masks = hkobj.parse_hotkeylist(hk) 342 | reg_hk_res = user32.RegisterHotKey(None, 1, masks, keycode) 343 | # time.sleep(0.1) 344 | if reg_hk_res: 345 | user32.UnregisterHotKey(None, reg_hk_res) 346 | else: 347 | return False, '热键被占用' 348 | except Exception as e: 349 | return False, e 350 | 351 | return True, '热键可被注册' 352 | 353 | def monitor_command(self): 354 | """ 监听执行命令 """ 355 | if not configr.is_ctxmenu_enabled(self.__config): 356 | return 357 | 358 | cmd_func_map = {} 359 | 360 | if configr.is_ctxmenu_prev_enabled(self.__config): 361 | cmd_func_map[argsdef.ARG_CMD_TYPE_PRE] = self.prev_bg 362 | 363 | if configr.is_ctxmenu_next_enabled(self.__config): 364 | cmd_func_map[argsdef.ARG_CMD_TYPE_NXT] = self.next_bg 365 | 366 | if configr.is_ctxmenu_favorite_enabled(self.__config): 367 | cmd_func_map[argsdef.ARG_CMD_TYPE_FAV] = self.favorite_bg 368 | 369 | if configr.is_ctxmenu_locate_enabled(self.__config): 370 | cmd_func_map[argsdef.ARG_CMD_TYPE_LOC] = self.locate_bg 371 | 372 | if len(cmd_func_map) > 0: 373 | sma = SimpleMmapActuator() 374 | sma.set_cmd_func_map(cmd_func_map) 375 | sma.run_monitor() 376 | log.info('开启命令执行器:监听可执行命令:{}'.format(cmd_func_map.keys())) 377 | -------------------------------------------------------------------------------- /src/py/utils.py: -------------------------------------------------------------------------------- 1 | # -*-coding:utf-8-*- 2 | 3 | """ 4 | 工具模块 5 | 6 | @author Myles Yang 7 | """ 8 | import ctypes 9 | import imghdr 10 | import json 11 | import os 12 | import pickle 13 | import re 14 | import shutil 15 | import threading 16 | import time 17 | import typing 18 | import urllib.request 19 | import webbrowser 20 | from typing import List, Union 21 | 22 | import pythoncom 23 | import requests 24 | import win32api 25 | import win32com.client as win32com_client 26 | import win32con 27 | import win32gui 28 | import win32inet 29 | import win32inetcon 30 | import win32process 31 | import win32ui 32 | from requests import Response 33 | from win32comext.shell import shell, shellcon 34 | 35 | import const_config as const 36 | from vo import E 37 | 38 | user32 = ctypes.windll.user32 39 | 40 | 41 | def is_background_valid(file) -> bool: 42 | """ 43 | 判断壁纸是否可用(是否是允许的图片类型) 44 | 45 | :param file: 壁纸绝对位置 / 壁纸的file对象 46 | :return: (可用,文件后缀) 47 | """ 48 | try: 49 | # 这玩意有时正常格式图片会检测不出来 50 | file_ext = imghdr.what(file) 51 | # 图片后缀,这些基本够用了,实际在设置时windows会将图片转换为jpg格式 52 | # 这些后缀的图片经过测试都能设置为桌面背景 53 | valid_img_ext_patn = r'png|jp[e]{0,1}g|gif|bmp|tif' 54 | if file_ext and re.match(valid_img_ext_patn, file_ext): 55 | return True 56 | except: 57 | pass 58 | return False 59 | 60 | 61 | def set_background(img_abs_path: str) -> bool: 62 | """ 63 | windows设置桌面背景,最后一个参数:SPIF_UPDATEINIFILE(在原路径设置),win32con.SPIF_SENDWININICHANGE(复制图片到缓存) 64 | 如果路径无效,也会生成一张纯色的图片,设置的壁纸会临时存放在路径"%USERPROFILE%/AppData/Roaming/Microsoft/Windows/Themes/CachedFiles" 65 | """ 66 | try: 67 | win32gui.SystemParametersInfo(win32con.SPI_SETDESKWALLPAPER, img_abs_path, win32con.SPIF_SENDWININICHANGE) 68 | return True 69 | except: 70 | pass 71 | return False 72 | 73 | 74 | def set_background_fit(wallpaper_style=0, tile_wallpaper=0): 75 | """ 76 | 设置桌面背景契合度(交给用户自行设置) 77 | """ 78 | 79 | # 打开指定注册表路径 80 | sub_reg_path = "Control Panel\\Desktop" 81 | reg_key = win32api.RegOpenKeyEx(win32con.HKEY_CURRENT_USER, sub_reg_path, 0, win32con.KEY_SET_VALUE) 82 | # WallpaperStyle:2拉伸,6适应,10填充,22跨区,其他0 83 | win32api.RegSetValueEx(reg_key, "WallpaperStyle", 0, win32con.REG_SZ, str(wallpaper_style)) 84 | # TileWallpaper:1平铺,居中0,其他0 85 | win32api.RegSetValueEx(reg_key, "TileWallpaper", 0, win32con.REG_SZ, str(tile_wallpaper)) 86 | win32api.RegCloseKey(reg_key) 87 | 88 | 89 | def get_run_args(args: Union[dict, str]): 90 | """ 获取参数运行 """ 91 | if isinstance(args, str): 92 | return args 93 | str_args = '' 94 | for key in args: 95 | value = args.get(key) 96 | str_args += ' {} {}'.format(key, value if value else '') 97 | return str_args 98 | 99 | 100 | def get_shortcut(shortcut) -> str: 101 | """ 102 | 获取快捷方式 103 | :param shortcut: 快捷方式对象或路径 104 | :return: 快捷方式指向的文件绝对路径 105 | """ 106 | if not is_main_thread(): 107 | pythoncom.CoInitialize() 108 | try: 109 | ws = win32com_client.Dispatch("WScript.Shell") 110 | return ws.CreateShortCut(shortcut).Targetpath 111 | except Exception as e: 112 | raise E().e(e, message="获取快捷方式[{}]指向目标失败".format(shortcut)) 113 | finally: 114 | if not is_main_thread(): 115 | pythoncom.CoUninitialize() 116 | 117 | 118 | def create_shortcut(filename: str, lnkname: str = None, args: str = None, style: int = None): 119 | """ 120 | 创建快捷方式 121 | :param filename: 目标文件名(需含路径) 122 | :param lnkname: 快捷方式名,可以是路径,也可以的包含路径的文件名(文件名需含后缀.lnk), 123 | 还可以是windows特殊路径,它必须以shell:开头,如shell:desktop表示桌面路径 124 | :param args: 启动程序的参数 125 | :param style: 窗口样式,1.Normal window普通窗口,3.Maximized最大化窗口,7.Minimized最小化 126 | :return: 快捷方式对象(快捷方式路径) 127 | """ 128 | target_path = os.path.abspath(filename) 129 | 130 | def get_lnkname(): 131 | fp, fn = os.path.split(target_path) 132 | mn, ext = os.path.splitext(fn) 133 | return mn + '.lnk' 134 | 135 | if not lnkname: 136 | lnkname = get_lnkname() 137 | else: 138 | if os.path.isdir(lnkname): 139 | lnkname = os.path.join(lnkname, get_lnkname()) 140 | else: 141 | if lnkname.lower().startswith('shell:'): 142 | # shell:desktop:lnkName 143 | sarr = lnkname.split(':') 144 | spec_dir = get_special_folders(sarr[1]) 145 | if spec_dir: 146 | ln = get_lnkname() 147 | if len(sarr) == 3: 148 | ln = sarr[2] if sarr[2].lower().endswith('.lnk') else sarr[2] + '.lnk' 149 | lnkname = os.path.join(spec_dir, ln) 150 | 151 | if not is_main_thread(): 152 | pythoncom.CoInitialize() 153 | try: 154 | ws = win32com_client.Dispatch("WScript.Shell") 155 | shortcut = ws.CreateShortCut(lnkname) 156 | shortcut.TargetPath = target_path 157 | shortcut.WorkingDirectory = os.path.split(target_path)[0] 158 | shortcut.Arguments = args.strip() if args else '' 159 | shortcut.WindowStyle = style if (style and style in [1, 3, 7]) else 1 160 | shortcut.Save() 161 | return shortcut 162 | except Exception as e: 163 | raise E().e(e, message="创建程序[{}]快捷方式失败".format(target_path)) 164 | finally: 165 | if not is_main_thread(): 166 | pythoncom.CoUninitialize() 167 | 168 | 169 | def get_special_folders(name: str) -> str: 170 | """ 171 | 获取 windows 特定目录路径 172 | :param name: windows目录特定值名称,如Desktop、Startup... 173 | """ 174 | if not is_main_thread(): 175 | pythoncom.CoInitialize() 176 | try: 177 | ws = win32com_client.Dispatch("WScript.Shell") 178 | return ws.SpecialFolders(name) 179 | except Exception as e: 180 | raise E().e(e, message="获取 windows 特定目录路径[{}]失败".format(name)) 181 | finally: 182 | if not is_main_thread(): 183 | pythoncom.CoUninitialize() 184 | 185 | 186 | def locate_path(path: str, is_select: bool = True) -> None: 187 | """ 188 | 打开资源管理器定位到相应路径 189 | :param path: 目标路径 190 | :param is_select: true定位选择目标,false打开目录 191 | """ 192 | if path and os.path.exists(path): 193 | os.popen('explorer /e,{} "{}"'.format( 194 | '/select,' if is_select else '', 195 | path 196 | )) 197 | 198 | 199 | def run_in_background(executable_target: str, args: str = ''): 200 | """ 201 | 后台运行目标程序 202 | :param executable_target: 可执行目标路径 203 | :param args: 参数 204 | :return: (bool: 是否成功, str:结果消息) 205 | """ 206 | if not is_main_thread(): 207 | pythoncom.CoInitialize() 208 | try: 209 | ws = win32com_client.Dispatch("WScript.Shell") 210 | executable_cmd = '"{}"{}'.format(executable_target, args) if args else '"{}"'.format(executable_target) 211 | res = ws.Run(executable_cmd, 0) 212 | return True, res 213 | except Exception as e: 214 | return False, str(e) 215 | finally: 216 | if not is_main_thread(): 217 | pythoncom.CoUninitialize() 218 | 219 | 220 | def set_foreground_window(target, block: bool = True) -> None: 221 | """ 222 | 设置 windows 窗口处于前台 223 | 224 | :param target: 窗口句柄或窗口标题 225 | :param block: 是否阻塞执行 226 | :return: 227 | """ 228 | if not target: 229 | return 230 | 231 | def f(): 232 | times_retry = 10 233 | while times_retry > 0: 234 | time.sleep(0.1) 235 | hwnd = None 236 | if isinstance(target, str): 237 | hwnd = win32gui.FindWindow(None, target) 238 | if hwnd and isinstance(target, int): 239 | try: 240 | win32gui.SetForegroundWindow(hwnd) 241 | finally: 242 | return 243 | times_retry -= 1 244 | 245 | if block: 246 | f() 247 | else: 248 | threading.Thread(target=lambda: f()).start() 249 | 250 | 251 | def create_dialog(message: str, title: str, style: int = win32con.MB_OK, 252 | block: bool = None, interval: float = 0, callback=None): 253 | """ 254 | 使用微软未公布的Windows API: MessageBoxTimeout 实现自动关闭的对话框,通过user32.dll调用, 255 | 相比于使用 MessageBox 来实现显得更加简洁,参数详情请参考以上函数 create_dialog 256 | 257 | 调用时 style 请不要 与 上 MB_SETFOREGROUND 258 | 259 | 值得注意的是 Windows 2000 没有导出该函数。并且对于多选一没有关闭/取消功能的对话框, 260 | 自动关闭时默认(回调)返回值为 32000 261 | 262 | :param message: 对话框消息内容 263 | :param title: 对话框标题 264 | :param style: 对话框类型,该值可以相加组合出不同的效果。 265 | :param block: 对话框是否阻塞调用线程,默认值取决于interval<=0,为Ture不会自动关闭,意味着阻塞调用线程 266 | :param interval: 对话框自动关闭秒数 267 | :param callback: 对话框关闭时的回调函数,含一参数为对话框关闭结果(按下的按钮值) 268 | :return: 当对话框为非阻塞时,无返回值(None),否则,对话框阻塞当前线程直到返回,值为按下的按钮值 269 | """ 270 | 271 | block = block if (block is not None) else interval <= 0 272 | interval = int(interval * 1000) if interval > 0 else 0 273 | 274 | def show(): 275 | # if UNICODE MessageBoxTimeoutW else MessageBoxTimeoutA 276 | # MessageBoxTimeout(hwnd, lpText, lpCaption, uType, wLanguageId, dwMilliseconds) 277 | btn_val = user32.MessageBoxTimeoutW(0, message, title, style | win32con.MB_SETFOREGROUND, 0, interval) 278 | if callback and callable(callback): 279 | callback(btn_val) 280 | return btn_val 281 | 282 | if block: 283 | return show() 284 | else: 285 | threading.Thread(target=show).start() 286 | 287 | 288 | def list_deduplication(li: list) -> list: 289 | """ 290 | 列表去重 291 | """ 292 | if not li: 293 | return list() 294 | res = list(set(li)) 295 | res.sort(key=li.index) 296 | return res 297 | 298 | 299 | def is_main_thread() -> bool: 300 | """ 301 | 检测当前线程是否为主线程 302 | """ 303 | return threading.current_thread() is threading.main_thread() 304 | 305 | 306 | def is_lock_workstation() -> bool: 307 | """ 308 | 判断Windows是否处于锁屏状态 309 | 目前暂时无法做到检测屏幕关屏状态 310 | """ 311 | hwnd = win32gui.GetForegroundWindow() 312 | return hwnd <= 0 313 | 314 | 315 | def get_process_path(pid: int) -> str: 316 | """ 317 | 获取进程路径(可能会因权限不足而获取失败) 318 | :param pid: 进程ID 319 | :return: 运行文件的绝对路径 320 | """ 321 | hwnd = None 322 | try: 323 | hwnd = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION | win32con.PROCESS_VM_READ, win32con.FALSE, pid) 324 | return win32process.GetModuleFileNameEx(hwnd, 0) 325 | except: 326 | return None 327 | finally: 328 | if hwnd: win32api.CloseHandle(hwnd) 329 | 330 | 331 | def is_process_running(pid: int, ppath: str = None): 332 | """ 判断进程是否在运行 333 | :param pid: 进程ID 334 | :param ppath: 程序绝对路径,根据pid获取程序路径,在对比 335 | """ 336 | try: 337 | is_running = win32process.GetProcessVersion(pid) > 0 338 | if is_running and ppath: 339 | path = get_process_path(pid) 340 | if not path: 341 | return False 342 | return os.path.normcase(path) == os.path.normcase(ppath) 343 | else: 344 | return is_running 345 | except: 346 | return False 347 | 348 | 349 | def select_folder(msg: str = "选择文件夹") -> str: 350 | """ 351 | 选择文件夹 352 | :param msg: 描述信息 353 | :return: 选择的文件夹路径 354 | """ 355 | """ SHBrowseForFolder: 356 | HWND hwndOwner; // 父窗口句柄 357 | LPCITEMIDLIST pidlRoot; // 要显示的文件目录对话框的根(Root) 358 | LPCTSTR lpszTitle; // 显示位于对话框左上部的标题 359 | UINT ulFlags; // 指定对话框的外观和功能的标志 360 | BFFCALLBACK lpfn; // 处理事件的回调函数 361 | LPARAM lParam; // 应用程序传给回调函数的参数 362 | """ 363 | try: 364 | pidl, display_name, image_list = shell.SHBrowseForFolder( 365 | win32gui.GetForegroundWindow(), # win32gui.GetDesktopWindow(), 366 | shell.SHGetFolderLocation(0, shellcon.CSIDL_DESKTOP, 0, 0), 367 | msg, 368 | shellcon.BIF_RETURNONLYFSDIRS | 0x00000040, 369 | None, 370 | None 371 | ) 372 | if pidl is None: 373 | return None 374 | else: 375 | path = shell.SHGetPathFromIDList(pidl) 376 | pathD = None 377 | try: 378 | pathD = path.decode('gbk') 379 | except Exception as e: 380 | try: 381 | pathD = path.decode('utf-8') 382 | except Exception as e: 383 | raise E().e(e, message="文件夹路径解密错误") 384 | return pathD 385 | except Exception as e: 386 | raise E().e(e, message="打开选择文件夹选择对话框失败") 387 | 388 | 389 | def select_file(): 390 | """ CreateFileDialog: 391 | bFileOpen; 392 | defExt; 393 | fileName; 394 | flags; 395 | filter; 396 | parent; 397 | """ 398 | dlg = win32ui.CreateFileDialog(1, None, None, win32con.OFN_HIDEREADONLY | win32con.OFN_OVERWRITEPROMPT, None, None) 399 | dlg.SetOFNInitialDir('C:') 400 | dlg.SetOFNTitle("选择文件") 401 | flag = dlg.DoModal() 402 | 403 | filename = dlg.GetPathName() # 获取选择的文件名称 404 | print(filename) 405 | 406 | 407 | def simple_request_get(url: str, params=None, timeout: int = 60) -> Response: 408 | """ 简单 get 请求 """ 409 | return requests.get(url, params=params, headers=const.headers, 410 | proxies=urllib.request.getproxies(), timeout=(5, timeout)) 411 | 412 | 413 | def open_url(url: str) -> None: 414 | """ 默认浏览器打开链接 """ 415 | webbrowser.open(url) 416 | 417 | 418 | def to_json_str(obj: object) -> str: 419 | """ class对象转json字符串 """ 420 | return json.dumps(obj.__dict__, indent=4, ensure_ascii=False) 421 | 422 | 423 | def kill_process(pids: List[int], kill_tree: bool = False) -> str: 424 | """ 杀死进程 """ 425 | joins = '' 426 | for pid in pids: 427 | joins += ' -pid {}'.format(pid) 428 | cmd = 'taskkill -f {} {}'.format('-t' if kill_tree else '', joins) 429 | result = os.popen(cmd) 430 | return result.read() 431 | 432 | 433 | def getpid() -> int: 434 | """ 获取主进程PID """ 435 | return os.getppid() 436 | 437 | 438 | def get_request_params(url: str) -> dict: 439 | """ 440 | 从URL中获取请求参数 441 | :param url: 442 | :return: 参数元组列表 list of (name, value) tuples 443 | """ 444 | query = urllib.parse.urlsplit(url).query 445 | query = dict(urllib.parse.parse_qsl(query)) 446 | 447 | # 移除空值 448 | dict_res = {k: v for k, v in query.items() if v} 449 | 450 | return dict_res 451 | 452 | 453 | def serialize(tar: typing.Any) -> str: 454 | """ 对象序列化保存为字节字符串:None -> b'\x80\x04N.' """ 455 | b = pickle.dumps(tar) 456 | return b.__str__() 457 | 458 | 459 | def deserialize(tar: str) -> typing.Any: 460 | """ 字节字符串反序列化为对象:b'\x80\x04N.' -> None """ 461 | b = eval('bytes({})'.format(tar)) 462 | return pickle.loads(b) 463 | 464 | 465 | def rm_file_dir(path: str) -> bool: 466 | """ 删除文件夹或文件 """ 467 | try: 468 | if os.path.isdir(path): 469 | shutil.rmtree(path) 470 | if os.path.isfile(path): 471 | os.remove(path) 472 | return True 473 | except: 474 | return False 475 | 476 | 477 | def get_split_value(opt_val: str, delimiter: str, opt_type: type = str) -> list: 478 | """ 479 | 获取多值分割项,通常由分隔符分割每一个值 480 | 481 | :param opt_val: 配置项字符值 482 | :param delimiter: 分割符 483 | :param opt_type: 配置的项每个值的数据类型,作为类型转换 484 | :return list: 配置项的每个值 485 | """ 486 | if not (opt_val or delimiter): 487 | return [] 488 | opt_vals = opt_val.split(delimiter) 489 | # 去除空白字符 490 | opt_vals = list(map(lambda s: s.strip(), opt_vals)) 491 | # 去除空白项 492 | opt_vals = list(filter(lambda s: s, opt_vals)) 493 | # 类型转换 494 | try: 495 | opt_vals = list(map(lambda s: opt_type(s), opt_vals)) 496 | except: 497 | return [] 498 | return opt_vals 499 | 500 | 501 | def get_path_size(path: str) -> int: 502 | """ 503 | 获取文件夹或者文件大小 504 | :param path: 文件夹 / 文件 505 | :return: 字节 506 | """ 507 | size = 0 508 | if not path or not os.path.exists(path): 509 | pass 510 | elif os.path.isdir(path): 511 | for root, dirs, files in os.walk(path): 512 | size += sum([os.path.getsize(os.path.join(root, name)) for name in files]) 513 | else: 514 | size = os.path.getsize(path) 515 | return size 516 | 517 | 518 | def get_win_valid_filename(filename: str, sub: str = '') -> str: 519 | """ 获取windows合法文件名 """ 520 | patn = r'[\\/:*?"<>|\r\n]+' 521 | if sub and re.match(patn, sub): sub = '' 522 | return re.sub(patn, sub, filename) 523 | 524 | 525 | def is_network_available() -> bool: 526 | """ 判断网络是否连通 """ 527 | # try: 528 | # exit_code = os.system('ping baidu.com') 529 | # return exit_code == 0 530 | # except: 531 | # return False 532 | try: 533 | win32inet.InternetCheckConnection('https://www.baidu.com', win32inetcon.FLAG_ICC_FORCE_CONNECTION, 0) 534 | return True 535 | except: 536 | return False 537 | 538 | 539 | def set_win_title(title: str, pid: int = None): 540 | """ 设置窗口标题:不可用 """ 541 | if not pid: 542 | try: 543 | win32api.SetConsoleTitle(title) 544 | return True 545 | except: 546 | return False 547 | hwnd = None 548 | try: 549 | hwnd = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION, False, pid) 550 | win32gui.SetWindowText(hwnd, title) 551 | return True 552 | except: 553 | return False 554 | finally: 555 | if hwnd: win32api.CloseHandle(hwnd) 556 | # hwnd = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION, False, 5676) 557 | # print(win32process.GetProcessId(hwnd)) 558 | # win32api.CloseHandle(hwnd) 559 | 560 | 561 | def create_desktop_context_menu(sub_key: str, values: typing.Dict[str, str]): 562 | """ 563 | 创建桌面上下文菜单 564 | 565 | :param sub_key: 566 | :param values: REG_SZ value 567 | """ 568 | if not sub_key: return False 569 | reg_key = None 570 | try: 571 | reg_key = win32api.RegCreateKey(win32con.HKEY_CLASSES_ROOT, "DesktopBackground\\Shell\\" + sub_key) 572 | for key in values: 573 | win32api.RegSetValueEx(reg_key, str(key), 0, win32con.REG_SZ, str(values[key])) 574 | return True 575 | except Exception as e: 576 | return False 577 | finally: 578 | if reg_key: win32api.RegCloseKey(reg_key) 579 | 580 | 581 | def delete_desktop_context_menu(sub_key: str): 582 | """ 删除桌面上下文菜单 """ 583 | if not sub_key: return False 584 | reg_key = None 585 | try: 586 | reg_key = win32api.RegOpenKeyEx(win32con.HKEY_CLASSES_ROOT, "DesktopBackground\\Shell", 587 | 0, win32con.KEY_ALL_ACCESS) 588 | win32api.RegDeleteTree(reg_key, sub_key) 589 | return True 590 | except Exception as e: 591 | return False 592 | finally: 593 | if reg_key: win32api.RegCloseKey(reg_key) 594 | -------------------------------------------------------------------------------- /src/py/vo.py: -------------------------------------------------------------------------------- 1 | # -*-coding:utf-8-*- 2 | 3 | """ 4 | 视图对象 5 | 6 | @author Myles Yang 7 | """ 8 | import traceback 9 | import typing 10 | from enum import Enum 11 | 12 | from starlette.responses import JSONResponse 13 | 14 | 15 | class ConfigVO(object): 16 | """ """ 17 | key: str 18 | value: typing.Any 19 | pytype: type 20 | defaults: typing.Any # 默认值,pcikle序列化字节字符串值 21 | comment: str 22 | enable: bool 23 | utime: str 24 | ctime: str 25 | 26 | def __init__(self, 27 | key: str = None, 28 | value: typing.Any = None, 29 | enable: bool = None 30 | ): 31 | self.key = key 32 | self.value = value 33 | self.enable = enable 34 | self.pytype = type(value).__name__ 35 | self.defaults = None 36 | self.comment = '' 37 | self.enable = True 38 | 39 | 40 | class StatusVO(object): 41 | """ """ 42 | running: bool 43 | 44 | 45 | class RS(Enum): 46 | """ 自定义响应状态 """ 47 | 48 | NOT_CURRENT_CLIENT = (401, '非最近请求客户端') 49 | NOT_FOUND = (404, '请求目标不存在') 50 | 51 | 52 | class E(Exception): 53 | """ 54 | 自定义异常 55 | """ 56 | status = 400 57 | message = 'error' 58 | cause = '' 59 | data = None 60 | 61 | def rs(self, rs: RS, data: typing.Any = None): 62 | self.status = rs.value[0] 63 | self.message = rs.value[1] 64 | self.data = data 65 | return self 66 | 67 | def ret(self, status: int = 400, message: str = None, data: typing.Any = None): 68 | self.status = status 69 | self.message = message 70 | self.data = data 71 | return self 72 | 73 | def e(self, e: Exception, message: str = None, data: typing.Any = None): 74 | tb = traceback.format_tb(e.__traceback__) 75 | if tb: 76 | self.cause = tb[0] 77 | self.message = message if message else e.__str__() 78 | self.data = data 79 | return self 80 | 81 | 82 | class R(object): 83 | """ 84 | 自定义返回体 85 | """ 86 | 87 | content = { 88 | 'status': 200, 89 | 'message': 'success', 90 | 'data': None 91 | } 92 | 93 | def ok(self, message: str = 'success', data: typing.Any = None): 94 | self.content['status'] = 200 95 | self.content['message'] = message 96 | self.content['data'] = data 97 | return JSONResponse(content=self.content) 98 | 99 | def err(self, message: str = 'fail', data: typing.Any = None): 100 | self.content['status'] = 400 101 | self.content['message'] = message 102 | self.content['data'] = data 103 | return JSONResponse(content=self.content) 104 | 105 | def rs(self, rs: RS, data: typing.Any = None): 106 | self.content['status'] = rs.value[0] 107 | self.content['message'] = rs.value[1] 108 | self.content['data'] = data 109 | return JSONResponse(content=self.content) 110 | 111 | def ret(self, status: int, message, data: typing.Any = None): 112 | self.content['status'] = status 113 | self.content['message'] = message 114 | self.content['data'] = data 115 | return JSONResponse(content=self.content) 116 | 117 | def e(self, e: E): 118 | self.content['status'] = e.status 119 | self.content['message'] = e.message 120 | self.content['data'] = e.data 121 | return JSONResponse(content=self.content) 122 | -------------------------------------------------------------------------------- /src/py/webapp.py: -------------------------------------------------------------------------------- 1 | # -*-coding:utf-8-*- 2 | 3 | """ 4 | WEB 服务器,使用fastapi 5 | 6 | @author Myles Yang 7 | """ 8 | import os 9 | 10 | import uvicorn 11 | import win32con 12 | from fastapi import FastAPI, Request, Response 13 | from fastapi.exceptions import StarletteHTTPException 14 | from fastapi.middleware.cors import CORSMiddleware 15 | from starlette.staticfiles import StaticFiles 16 | 17 | import args_definition as argsdef 18 | import configurator as configr 19 | import const_config as const 20 | import utils 21 | from component import CustomLogger 22 | from controller import index as index_router, api as api_router, init_app_breathe 23 | from vo import R, RS, E 24 | 25 | run_args = argsdef.arg_dict 26 | 27 | log = CustomLogger().get_logger() 28 | 29 | XToken = None # 客户端唯一标识,仅允许一个客户端进行请求 30 | 31 | 32 | def __init_logger(): 33 | """ 初始化WEBUI Logger (说明:webui另起一个进程,日志单例失效)""" 34 | logger = CustomLogger() 35 | logger.set_logpath(configr.get_log_abspath()) 36 | logger.use_file_logger() 37 | 38 | 39 | # web服务启动回调 40 | def __on_startup(): 41 | log.info('WEBUI服务启动:{}'.format(const.server)) 42 | init_app_breathe() 43 | if run_args.get(argsdef.ARG_KEY_ENV) == argsdef.ARG_ENV_TYPE_PROD: 44 | __open_webui() 45 | configr.record_fpid(True) 46 | 47 | 48 | # web服务关闭回调,windwos杀死进程似乎不会发出信号,下面代码应该不会执行 49 | def __on_shutdown(): 50 | log.info('WEBUI服务关闭:{}'.format(const.server)) 51 | configr.record_fpid(False) 52 | 53 | 54 | app = FastAPI(title=const.app_name, 55 | on_startup=[__init_logger, __on_startup], 56 | on_shutdown=[__on_shutdown]) 57 | 58 | # webui静态资源映射 59 | __webui_path = os.path.join(const.app_dirname, 'webui') 60 | os.makedirs(__webui_path, exist_ok=True) 61 | app.mount("/webui", StaticFiles(directory=__webui_path), name="webui") 62 | 63 | 64 | def __open_webui(): 65 | utils.open_url('{}/webui'.format(const.server)) 66 | 67 | 68 | @app.middleware("http") 69 | async def interceptor(request: Request, call_next): 70 | """ 71 | 拦截器定义 72 | """ 73 | if request.url.path.startswith('/api/'): # 拦截api接口 74 | # 检查请求头 75 | global XToken 76 | x_init = request.headers.get('X-Init') 77 | if x_init: 78 | XToken = x_init 79 | return R().ok(message="设置客户端唯一标识", data=x_init) 80 | x_token = request.headers.get('X-Token') 81 | if not x_token or x_token != XToken: 82 | return R().rs(RS.NOT_CURRENT_CLIENT) 83 | response: Response = await call_next(request) 84 | return response 85 | 86 | 87 | """ 跨域配置,必须置于拦截器之后,否则请求被拦截时无法携带允许跨域的请求头而出现跨域报错 88 | """ 89 | app.add_middleware( 90 | CORSMiddleware, 91 | allow_origins=["*"], 92 | allow_credentials=False, 93 | allow_methods=["*"], 94 | allow_headers=["*"] 95 | ) 96 | 97 | 98 | @app.exception_handler(StarletteHTTPException) 99 | async def not_found(request: Request, exc): 100 | return R().rs(RS.NOT_FOUND, {'path': request.url.path}) 101 | 102 | 103 | @app.exception_handler(E) 104 | async def error(request: Request, exc: E): 105 | log.error("请求路径:{},错误:{}\n{}".format(request.url.path, exc.message, exc.cause)) 106 | return R().e(exc) 107 | 108 | 109 | # 路由注册 110 | app.include_router(index_router) 111 | app.include_router(api_router) 112 | 113 | 114 | def run_app(): 115 | # 检测服务是否可用 116 | # try: 117 | # resp = utils.simple_request_get(const.server) 118 | # if resp and resp.status_code == 200: 119 | # __open_webui() 120 | # return 121 | # except: 122 | # pass 123 | import application as app 124 | if utils.is_process_running(configr.get_fpid(), app.app_fullpath): 125 | __open_webui() 126 | return 127 | try: 128 | # 进程启动 129 | if run_args.get(argsdef.ARG_KEY_ENV) == argsdef.ARG_ENV_TYPE_PROD: 130 | uvicorn.run("webapp:app", host=const.host, port=const.port) 131 | else: 132 | uvicorn.run("webapp:app", host=const.host, port=const.port, reload=True, debug=True) 133 | except Exception as e: 134 | log.error('WEBUI服务启动失败,{}'.format(e)) 135 | utils.create_dialog("WEBUI服务启动失败!", const.dialog_title, 136 | style=win32con.MB_ICONERROR, interval=7, 137 | callback=lambda x: utils.kill_process([os.getpid()])) 138 | -------------------------------------------------------------------------------- /src/vue/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 24 | 25 | 60 | 61 | -------------------------------------------------------------------------------- /src/vue/api/index.js: -------------------------------------------------------------------------------- 1 | import request, { baseURL } from '../util/request' 2 | import { uuid } from '../util/common' 3 | import axios from 'axios' 4 | 5 | export const wallhaven_url = 'https://wallhaven.cc' 6 | 7 | /** 8 | * 连接服务,认证 9 | * @return {AxiosPromise} 10 | */ 11 | export function connect() { 12 | return request({ 13 | url: '/connect', 14 | method: 'get', 15 | headers: { 'X-Init': uuid() } 16 | }) 17 | } 18 | 19 | /** 20 | * 获取 wallhaven 壁纸列表数据 21 | * @data data 22 | * @returns {AxiosPromise} 23 | */ 24 | export function getBgs(data) { 25 | return request({ 26 | method: 'post', 27 | url: '/wallhaven', 28 | data 29 | }) 30 | } 31 | 32 | /** 33 | * 用来验证apikey是否可以 34 | * @return {Promise>} 35 | * @param data 36 | */ 37 | export function getBgsWF(data) { 38 | return request({ 39 | method: 'post', 40 | url: '/wallhaven', 41 | timeout: 5000, 42 | data 43 | }) 44 | } 45 | 46 | /** 47 | * App心跳 48 | * @return {AxiosPromise} 49 | */ 50 | export function breathe() { 51 | return request({ 52 | url: '/breathe', 53 | method: 'get' 54 | }) 55 | } 56 | 57 | /** 58 | * 选择工作目录 59 | * @return {AxiosPromise} 60 | */ 61 | export function selectFolder() { 62 | return request({ 63 | url: '/config/workdir', 64 | method: 'get' 65 | }) 66 | } 67 | 68 | /** 69 | * 获取配置信息 70 | * @return {AxiosPromise} 71 | */ 72 | export function getConfig() { 73 | return request({ 74 | url: '/config', 75 | method: 'get' 76 | }) 77 | } 78 | 79 | /** 80 | * 更新配置信息 81 | * @return {AxiosPromise} 82 | */ 83 | export function updateConfig(data) { 84 | return request({ 85 | url: '/config', 86 | method: 'post', 87 | data 88 | }) 89 | } 90 | 91 | /** 92 | * 获取状态信息 93 | * @return {AxiosPromise} 94 | */ 95 | export function getStatus() { 96 | return request({ 97 | url: '/status', 98 | method: 'get' 99 | }) 100 | } 101 | 102 | /** 103 | * 创建桌面快捷方式 104 | * @return {AxiosPromise} 105 | */ 106 | export function createDesktopLnk() { 107 | return request({ 108 | url: '/create-desktop-lnk', 109 | method: 'get' 110 | }) 111 | } 112 | 113 | /** 114 | * 打开收藏文件夹 115 | * @return {AxiosPromise} 116 | */ 117 | export function locateFavoritePath() { 118 | return request({ 119 | url: '/locate-favorite-path', 120 | method: 'get' 121 | }) 122 | } 123 | 124 | /** 125 | * 切换程序开关 126 | * @return {AxiosPromise} 127 | */ 128 | export function toggleUd() { 129 | return request({ 130 | url: '/toggle-ud', 131 | method: 'get' 132 | }) 133 | } 134 | -------------------------------------------------------------------------------- /src/vue/asset/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snwjas/RandomDesktopBackground-WEBUI/1e34fb38a1088687987a9c2acd0ce5386b70489c/src/vue/asset/background.jpg -------------------------------------------------------------------------------- /src/vue/asset/bg-dark-grain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snwjas/RandomDesktopBackground-WEBUI/1e34fb38a1088687987a9c2acd0ce5386b70489c/src/vue/asset/bg-dark-grain.png -------------------------------------------------------------------------------- /src/vue/asset/blue-gradients.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snwjas/RandomDesktopBackground-WEBUI/1e34fb38a1088687987a9c2acd0ce5386b70489c/src/vue/asset/blue-gradients.jpg -------------------------------------------------------------------------------- /src/vue/asset/icon/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; /* Project id 2870681 */ 3 | src: url('iconfont.woff2?t=1634288717914') format('woff2'), 4 | url('iconfont.woff?t=1634288717914') format('woff'), 5 | url('iconfont.ttf?t=1634288717914') format('truetype'); 6 | } 7 | 8 | .iconfont { 9 | font-family: "iconfont" !important; 10 | font-size: 16px; 11 | font-style: normal; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | .icon-shezhi:before { 17 | content: "\e64b"; 18 | } 19 | 20 | .icon-qidong:before { 21 | content: "\e648"; 22 | } 23 | 24 | .icon-guanyuwomen:before { 25 | content: "\e635"; 26 | } 27 | 28 | .icon-ceshi:before { 29 | content: "\e632"; 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/vue/asset/icon/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snwjas/RandomDesktopBackground-WEBUI/1e34fb38a1088687987a9c2acd0ce5386b70489c/src/vue/asset/icon/iconfont.ttf -------------------------------------------------------------------------------- /src/vue/asset/icon/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snwjas/RandomDesktopBackground-WEBUI/1e34fb38a1088687987a9c2acd0ce5386b70489c/src/vue/asset/icon/iconfont.woff -------------------------------------------------------------------------------- /src/vue/asset/icon/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snwjas/RandomDesktopBackground-WEBUI/1e34fb38a1088687987a9c2acd0ce5386b70489c/src/vue/asset/icon/iconfont.woff2 -------------------------------------------------------------------------------- /src/vue/asset/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snwjas/RandomDesktopBackground-WEBUI/1e34fb38a1088687987a9c2acd0ce5386b70489c/src/vue/asset/logo.png -------------------------------------------------------------------------------- /src/vue/asset/style.scss: -------------------------------------------------------------------------------- 1 | @mixin scrollBar { 2 | &::-webkit-scrollbar { 3 | width: 7px; 4 | } 5 | &::-webkit-scrollbar-thumb { 6 | background: #64a5ff; 7 | } 8 | &::-webkit-scrollbar-track { 9 | background: #E8EAF0; 10 | } 11 | //&::-webkit-scrollbar-track-piece { 12 | // background: #d3dce6; 13 | //} 14 | } 15 | 16 | * { 17 | padding: 0; 18 | margin: 0; 19 | } 20 | 21 | body { 22 | height: 100%; 23 | text-rendering: optimizeLegibility; 24 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; 25 | } 26 | 27 | html { 28 | height: 100%; 29 | box-sizing: border-box; 30 | @include scrollBar; 31 | } 32 | 33 | #app, .container { 34 | width: 100%; 35 | height: 100%; 36 | } 37 | 38 | *, *:before, *:after { 39 | box-sizing: inherit; 40 | } 41 | 42 | a { 43 | text-decoration: none; 44 | outline: none; 45 | color: #409eff; 46 | opacity: .7; 47 | 48 | &:hover { 49 | opacity: 1; 50 | cursor: pointer; 51 | } 52 | } 53 | 54 | div:focus { 55 | outline: none; 56 | } 57 | 58 | .clearfix { 59 | &:after { 60 | content: ""; 61 | display: table; 62 | clear: both; 63 | } 64 | } 65 | 66 | // element-u样式调整 67 | .el-dialog__header { 68 | padding: 1.04vw 1.04vw 0.52vw 1.04vw; 69 | } 70 | 71 | .el-dialog__title { 72 | font-size: 0.9375vw; 73 | } 74 | 75 | .el-dialog__body { 76 | padding: 1.04vw; 77 | } 78 | 79 | .el-message { 80 | min-width: 15.625vw; 81 | } 82 | 83 | .el-col-4-8 { 84 | width: 20%; 85 | } 86 | 87 | .el-col-lg-4-8 { 88 | width: 20%; 89 | } 90 | -------------------------------------------------------------------------------- /src/vue/component/Background.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 77 | 78 | 107 | -------------------------------------------------------------------------------- /src/vue/component/Image.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 111 | 112 | 250 | -------------------------------------------------------------------------------- /src/vue/component/Tips.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Tips from './Tips.vue' 3 | 4 | const Constructor = Vue.extend(Tips) 5 | 6 | const GlobalTips = function(options = {}) { 7 | const instance = new Constructor() 8 | instance.$mount() 9 | document.body.appendChild(instance.$el) 10 | // set options start 11 | instance.tipsName = options.tipsName || 'default' 12 | if (options.background) { 13 | instance.background = options.background 14 | } 15 | if (options.text) { 16 | instance.text = options.text 17 | } 18 | if (options.spinner) { 19 | instance.spinner = options.spinner 20 | } 21 | if (options.customClass) { 22 | instance.customClass = options.customClass 23 | } 24 | if (options.buttons && options.buttons.length > 0) { 25 | instance.buttons = options.buttons 26 | } 27 | // set options end 28 | Vue.prototype.$tipsInstance = instance 29 | instance.getCurrentInstance = () => { return Vue.prototype.$tipsInstance } 30 | instance.close = () => { 31 | Vue.prototype.$tipsInstance = undefined 32 | document.body.removeChild(instance.$el) 33 | } 34 | return instance 35 | } 36 | 37 | export default GlobalTips 38 | -------------------------------------------------------------------------------- /src/vue/component/Tips.vue: -------------------------------------------------------------------------------- 1 | 2 | 28 | 29 | 82 | 83 | 131 | -------------------------------------------------------------------------------- /src/vue/component/Typewriter.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 90 | 91 | 112 | -------------------------------------------------------------------------------- /src/vue/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router/index.js' 4 | import store from './store/index.js' 5 | import { breathe } from './api' 6 | import GlobalTips from './component/Tips.js' 7 | 8 | import ElementUI from 'element-ui' 9 | import 'element-ui/lib/theme-chalk/index.css' 10 | 11 | import './asset/style.scss' // 全局样式 12 | import './asset/icon/iconfont.css' // 图标 13 | 14 | Vue.use(ElementUI) 15 | 16 | // 全局组件 17 | Vue.prototype.$tips = GlobalTips 18 | 19 | // 禁用按键 20 | document.oncontextmenu = () => { return false } 21 | document.onkeydown = (e) => { 22 | const kc = e.keyCode 23 | if (kc === 123) { return false } 24 | if ((e.ctrlKey) && (kc === 83)) { return false } 25 | } 26 | 27 | const appName = '随机桌面壁纸' 28 | router.beforeEach(async(to, from, next) => { 29 | // 修改页面title 30 | document.title = to.meta && to.meta.title ? `${to.meta.title} | ${appName}` : appName 31 | try { 32 | if (!store.getters.token) { 33 | await store.dispatch('app/getToken').then(async() => { 34 | const tips = Vue.prototype.$tipsInstance 35 | if (tips && tips.tipsName === 'NOT_CURRENT_CLIENT') { 36 | tips.close() 37 | } 38 | await store.dispatch('app/getStatus') 39 | await store.dispatch('app/getConfig') 40 | const loop = setInterval(() => { 41 | breathe().then(resp => { 42 | if (!(typeof resp === 'boolean' && resp)) { 43 | clearInterval(loop) 44 | } 45 | }).catch(() => { 46 | clearInterval(loop) 47 | }) 48 | }, 60000) 49 | }) 50 | } 51 | if (store.getters.token && Object.keys(store.getters.status).length === 0) { 52 | await store.dispatch('app/getStatus') 53 | } 54 | if (store.getters.token && Object.keys(store.getters.config).length === 0) { 55 | await store.dispatch('app/getConfig') 56 | } 57 | } catch (e) { 58 | // Message.error(e || 'Has Error!') 59 | } 60 | next() 61 | }) 62 | router.afterEach(() => { 63 | 64 | }) 65 | 66 | new Vue({ 67 | el: '#app', 68 | router, 69 | store, 70 | render: h => h(App) 71 | }) 72 | 73 | -------------------------------------------------------------------------------- /src/vue/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snwjas/RandomDesktopBackground-WEBUI/1e34fb38a1088687987a9c2acd0ce5386b70489c/src/vue/public/favicon.ico -------------------------------------------------------------------------------- /src/vue/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | 随机桌面壁纸 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/vue/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | export default new Router({ 7 | routes: [ 8 | { 9 | path: '/404', 10 | component: () => import('../view/404.vue'), 11 | meta: { title: '404' }, 12 | hidden: true 13 | }, 14 | // 首页 15 | { 16 | path: '/', 17 | name: 'Index', 18 | meta: { title: '' }, 19 | component: () => import('../view/Home.vue') 20 | // component: () => import('../component/Tips.vue') 21 | }, 22 | { 23 | path: '/type', 24 | name: 'Type', 25 | meta: { title: '设置wallhaven图源' }, 26 | component: () => import('../view/Type.vue') 27 | }, 28 | // 404 page must be placed at the end !!! 29 | { path: '*', redirect: '/404', hidden: true } 30 | ] 31 | }) 32 | -------------------------------------------------------------------------------- /src/vue/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | config: state => state.app.config, 3 | status: state => state.app.status, 4 | token: state => state.app.token 5 | } 6 | export default getters 7 | -------------------------------------------------------------------------------- /src/vue/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import getters from './getters' 4 | import app from './modules/app' 5 | 6 | Vue.use(Vuex) 7 | 8 | const store = new Vuex.Store({ 9 | modules: { 10 | app 11 | }, 12 | getters 13 | }) 14 | 15 | export default store 16 | -------------------------------------------------------------------------------- /src/vue/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import { connect, getConfig, getStatus } from '../../api/index' 2 | 3 | const state = () => { 4 | return { 5 | config: {}, 6 | status: {}, 7 | token: '' 8 | } 9 | } 10 | 11 | const mutations = { 12 | SET_CONFIG: (state, config) => { 13 | state.config = config 14 | }, 15 | SET_STATUS: (state, status) => { 16 | state.status = status 17 | }, 18 | SET_TOKEN: (state, token) => { 19 | state.token = token 20 | } 21 | } 22 | 23 | const actions = { 24 | getToken({ commit }) { 25 | return new Promise((resolve, reject) => { 26 | connect().then(resp => { 27 | commit('SET_TOKEN', resp) 28 | resolve(resp) 29 | }).catch(error => { 30 | reject(error) 31 | }) 32 | }) 33 | }, 34 | getConfig({ commit }) { 35 | return new Promise((resolve, reject) => { 36 | getConfig().then(resp => { 37 | commit('SET_CONFIG', resp) 38 | resolve(resp) 39 | }).catch(error => { 40 | reject(error) 41 | }) 42 | }) 43 | }, 44 | getStatus({ commit }) { 45 | return new Promise((resolve, reject) => { 46 | getStatus().then(resp => { 47 | commit('SET_STATUS', resp) 48 | resolve(resp) 49 | }).catch(error => { 50 | reject(error) 51 | }) 52 | }) 53 | } 54 | } 55 | 56 | export default { 57 | namespaced: true, 58 | state: state, 59 | mutations: mutations, 60 | actions: actions 61 | } 62 | 63 | -------------------------------------------------------------------------------- /src/vue/util/common.js: -------------------------------------------------------------------------------- 1 | import { Message } from 'element-ui' 2 | import axios from 'axios' 3 | 4 | // 睡眠函数,在调用的函数上需加上 async,调用它需要加 await 5 | // e.g. async function exec() { ... await sleep(1000) ... } 6 | export function sleep(ms) { 7 | return new Promise(resolve => setTimeout(resolve, ms)) 8 | } 9 | 10 | /** 11 | * 十六进制颜色转换为RGB颜色 12 | * @param color 13 | * @param isArr 14 | * @return string | [] 15 | */ 16 | export function colorHexToRGB(color, isArr) { 17 | color = color.toUpperCase() 18 | color = color.startsWith('#') ? color : '#' + color 19 | if (/^#[0-9a-fA-F]{3,6}/.test(color)) { 20 | const hexArray = [] 21 | let count = 1 22 | for (let i = 1; i <= 3; i++) { 23 | if (color.length - 2 * i > 3 - i) { 24 | hexArray.push(Number('0x' + color.substring(count, count + 2))) 25 | count += 2 26 | } else { 27 | hexArray.push(Number('0x' + color.charAt(count) + color.charAt(count))) 28 | count += 1 29 | } 30 | } 31 | return isArr ? hexArray : 'RGB(' + hexArray.join(',') + ')' 32 | } else { 33 | return color 34 | } 35 | } 36 | 37 | /** 38 | * 获取UUID 39 | * @returns {string} 40 | */ 41 | export function uuid() { 42 | const url = URL.createObjectURL(new Blob()) 43 | const uuid = url.toString() 44 | URL.revokeObjectURL(url) 45 | return uuid.substring(uuid.lastIndexOf('/') + 1) 46 | } 47 | 48 | /** 49 | * 字节单位转换 -> B|KB|MB|GB 50 | * @param {number} byte 字节数 51 | * @param {number} fixed 保留小数位数 52 | * @returns {string} 53 | */ 54 | export function convertBit(byte, fixed = 2) { 55 | byte = parseInt(byte) 56 | let size 57 | if (byte < 0.1 * 1024) { // 如果小于0.1KB转化成B 58 | size = byte.toFixed(fixed) + 'B' 59 | } else if (byte < 0.1 * 1024 * 1024) { // 如果小于0.1MB转化成KB 60 | size = (byte / 1024).toFixed(fixed) + 'KB' 61 | } else if (byte < 0.1 * 1024 * 1024 * 1024) { // 如果小于0.1GB转化成MB 62 | size = (byte / (1024 * 1024)).toFixed(fixed) + 'MB' 63 | } else { // 其他转化成GB 64 | size = (byte / (1024 * 1024 * 1024)).toFixed(fixed) + 'GB' 65 | } 66 | 67 | const sizeStr = size + '' 68 | const len = sizeStr.indexOf('\.') 69 | const dec = sizeStr.substring(len + 1, len + 3) 70 | if (dec === '00') { // 当小数点后为00时 去掉小数部分 71 | return sizeStr.substring(0, len) + sizeStr.substring(len + 3, 5) 72 | } 73 | return sizeStr 74 | } 75 | 76 | /** 77 | * 下载文件 78 | * @param url 79 | * @param fileName 80 | */ 81 | export function downloadFile(url, fileName) { 82 | axios.request({ 83 | url: url, 84 | method: 'get', 85 | responseType: 'blob', 86 | timeout: 1000 * 60 * 5 87 | }).then(resp => { 88 | const blob = resp.data 89 | if (window.navigator.msSaveOrOpenBlob) { 90 | navigator.msSaveBlob(blob, fileName) 91 | } else { 92 | const link = document.createElement('a') 93 | link.href = window.URL.createObjectURL(blob) 94 | link.download = fileName 95 | link.click() 96 | window.URL.revokeObjectURL(link.href) 97 | } 98 | }).catch((err) => { 99 | console.log(err) 100 | Message({ 101 | message: '文件下载失败', 102 | type: 'error', 103 | duration: 3.6 * 1000 104 | }) 105 | }) 106 | } 107 | 108 | /** 109 | * 判断是否为指向图片的URL 110 | * @param url 111 | */ 112 | export function isImageUrl(url) { 113 | return new Promise((resolve, reject) => { 114 | const img = new Image() 115 | img.onload = () => { 116 | resolve(true) 117 | } 118 | img.onerror = () => { 119 | reject(new Error('图片加载失败')) 120 | } 121 | img.src = url 122 | }) 123 | } 124 | 125 | /** 126 | * 字符串Hash值 127 | * @param str 128 | * @returns {number} 129 | */ 130 | export function hashCode(str) { 131 | let hash = 0 132 | if (!str || str.length === 0) return hash 133 | for (let i = 0; i < str.length; i++) { 134 | const chr = str.charCodeAt(i) 135 | hash = ((hash << 5) - hash) + chr 136 | hash |= 0 // Convert to 32bit integer 137 | } 138 | return hash 139 | } 140 | 141 | /** 142 | * 深拷贝 143 | * @param target 144 | * @return {{}} 145 | */ 146 | export function deepClone(target) { 147 | // 定义一个变量 148 | let result 149 | // 如果当前需要深拷贝的是一个对象的话 150 | if (typeof target === 'object') { 151 | // 如果是一个数组的话 152 | if (Array.isArray(target)) { 153 | result = [] // 将result赋值为一个数组,并且执行遍历 154 | for (const i in target) { 155 | // 递归克隆数组中的每一项 156 | result.push(deepClone(target[i])) 157 | } 158 | // 判断如果当前的值是null的话;直接赋值为null 159 | } else if (target === null) { 160 | result = null 161 | // 判断如果当前的值是一个RegExp对象的话,直接赋值 162 | } else if (target.constructor === RegExp) { 163 | result = target 164 | } else { 165 | // 否则是普通对象,直接for in循环,递归赋值对象的所有值 166 | result = {} 167 | for (const i in target) { 168 | result[i] = deepClone(target[i]) 169 | } 170 | } 171 | // 如果不是对象的话,就是基本数据类型,那么直接赋值 172 | } else { 173 | result = target 174 | } 175 | // 返回最终结果 176 | return result 177 | } 178 | 179 | /** 180 | * URL参数拼接 181 | * @param url e.g.https://www.baidu.com https://www.baidu.com?a=1& 182 | * @param data e.g.{b=2} 183 | * @return {String} 184 | */ 185 | export function formatURL(url, data) { 186 | let tmpURL = '' 187 | for (const k in data) { 188 | const value = data[k] !== undefined ? data[k] : '' 189 | tmpURL += ('&' + k + '=' + encodeURIComponent(value)) 190 | } 191 | const params = tmpURL ? tmpURL.substring(1) : '' 192 | url += ((url.indexOf('?') < 0 ? '?' : (url.endsWith('&') ? '' : '&')) + params) 193 | return url 194 | } 195 | 196 | /** 197 | * 获取URL参数 198 | * @param url url 199 | * @return {Object} 200 | */ 201 | export function getURLParams(url) { 202 | const params = {} 203 | const idx = url.indexOf('?') 204 | if (idx !== -1) { 205 | const str = url.substring(idx + 1, url.length) 206 | const strArr = str.split('&') 207 | for (let i = 0; i < strArr.length; i++) { 208 | params[strArr[i].split('=')[0]] = decodeURIComponent(strArr[i].split('=')[1]) 209 | } 210 | } 211 | return params 212 | } 213 | 214 | /** 215 | * 关闭当前标签页 216 | */ 217 | export function closeWebPage() { 218 | if (navigator.userAgent.indexOf('MSIE') > 0 && 219 | !navigator.userAgent.indexOf('MSIE 6.0') > 0) { // IE 6 + 220 | window.open('', '_top') 221 | window.top.close() 222 | return 223 | } 224 | window.location.href = 'about:blank' 225 | window.close() 226 | } 227 | -------------------------------------------------------------------------------- /src/vue/util/hkmap.js: -------------------------------------------------------------------------------- 1 | const hkMap = { 2 | '16': 'shift', 3 | '17': 'control', 4 | '18': 'alt', 5 | '91': 'super', 6 | '8': 'backspace', 7 | '9': 'tab', 8 | '13': 'return', 9 | '19': 'pause', 10 | '27': 'escape', 11 | '32': 'space', 12 | '33': 'kp_prior', 13 | '34': 'kp_next', 14 | '35': 'kp_end', 15 | '36': 'kp_home', 16 | '37': 'kp_left', 17 | '38': 'kp_up', 18 | '39': 'kp_right', 19 | '40': 'kp_down', 20 | '45': 'insert', 21 | '46': 'delete', 22 | '48': '0', 23 | '49': '1', 24 | '50': '2', 25 | '51': '3', 26 | '52': '4', 27 | '53': '5', 28 | '54': '6', 29 | '55': '7', 30 | '56': '8', 31 | '57': '9', 32 | '65': 'a', 33 | '66': 'b', 34 | '67': 'c', 35 | '68': 'd', 36 | '69': 'e', 37 | '70': 'f', 38 | '71': 'g', 39 | '72': 'h', 40 | '73': 'i', 41 | '74': 'j', 42 | '75': 'k', 43 | '76': 'l', 44 | '77': 'm', 45 | '78': 'n', 46 | '79': 'o', 47 | '80': 'p', 48 | '81': 'q', 49 | '82': 'r', 50 | '83': 's', 51 | '84': 't', 52 | '85': 'u', 53 | '86': 'v', 54 | '87': 'w', 55 | '88': 'x', 56 | '89': 'y', 57 | '90': 'z', 58 | '96': 'kp_0', 59 | '97': 'kp_1', 60 | '98': 'kp_2', 61 | '99': 'kp_3', 62 | '100': 'kp_4', 63 | '101': 'kp_5', 64 | '102': 'kp_6', 65 | '103': 'kp_7', 66 | '104': 'kp_8', 67 | '105': 'kp_9', 68 | '106': 'kp_multiply', 69 | '107': 'kp_add', 70 | '108': 'kp_separator', 71 | '109': 'kp_subtract', 72 | '110': 'kp_decimal', 73 | '111': 'kp_divide', 74 | '112': 'f1', 75 | '113': 'f2', 76 | '114': 'f3', 77 | '115': 'f4', 78 | '116': 'f5', 79 | '117': 'f6', 80 | '118': 'f7', 81 | '119': 'f8', 82 | '120': 'f9', 83 | '121': 'f10', 84 | '122': 'f11', 85 | '123': 'f12', 86 | '124': 'f13', 87 | '125': 'f14', 88 | '126': 'f15', 89 | '127': 'f16', 90 | '128': 'f17', 91 | '129': 'f18', 92 | '130': 'f19', 93 | '131': 'f20', 94 | '132': 'f21', 95 | '133': 'f22', 96 | '134': 'f23', 97 | '135': 'f24' 98 | } 99 | 100 | export default hkMap 101 | -------------------------------------------------------------------------------- /src/vue/util/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import store from '../store/index' 3 | import { Message } from 'element-ui' 4 | import { uuid } from './common' 5 | import Vue from 'vue' 6 | import GlobalTips from '../component/Tips' 7 | import { breathe } from '../api' 8 | 9 | export const baseURL = 'http://127.6.6.6:23333' 10 | 11 | // create an axios instance 12 | const service = axios.create({ 13 | baseURL: baseURL + '/api' 14 | // withCredentials: true, // send cookies when cross-domain requests 15 | // timeout: 7000 // request timeout 16 | }) 17 | 18 | // request interceptor 19 | service.interceptors.request.use(config => { 20 | if (store.getters.token) { 21 | config.headers['X-Token'] = store.getters.token 22 | } 23 | config.params = { ...config.params, rid: uuid() } 24 | return config 25 | }, error => { 26 | console.log(error) // for debug 27 | return Promise.reject(error) 28 | }) 29 | 30 | // response interceptor 31 | service.interceptors.response.use( 32 | response => { 33 | const resp = response.data 34 | if (resp.status === 200) { 35 | return resp.data 36 | } else if (resp.status === 401) { // 非最近请求客户端 37 | const tips = Vue.prototype.$tipsInstance 38 | console.log(tips) 39 | if (tips && tips.tipsName !== 'NOT_CURRENT_CLIENT') { 40 | tips.close() 41 | } 42 | GlobalTips({ 43 | tipsName: 'NOT_CURRENT_CLIENT', 44 | spinner: '', 45 | text: '与WEBUI服务端失去连接...', 46 | buttons: [{ 47 | text: '重新连接', 48 | click: async function() { 49 | await store.dispatch('app/getToken').then(async() => { 50 | const tips = Vue.prototype.$tipsInstance 51 | if (tips && tips.tipsName === 'NOT_CURRENT_CLIENT') { 52 | tips.close() 53 | } 54 | Message({ message: '已重新连接上WEBUI服务端', type: 'success', duration: 3.6 * 1000 }) 55 | await store.dispatch('app/getStatus') 56 | await store.dispatch('app/getConfig') 57 | const loop = setInterval(() => { 58 | breathe().then(resp => { 59 | if (!(typeof resp === 'boolean' && resp)) { 60 | clearInterval(loop) 61 | } 62 | }).catch(() => { 63 | clearInterval(loop) 64 | }) 65 | }, 60000) 66 | }) 67 | } 68 | }] 69 | }) 70 | return Promise.reject(new Error(resp.message || 'Error')) 71 | } else { 72 | const message = resp.message 73 | Message({ message: message || 'Error', type: 'error', duration: 3.6 * 1000 }) 74 | return Promise.reject(new Error(message || 'Error')) 75 | } 76 | }, 77 | error => { 78 | const errMsg = error.message 79 | if (/.*Network Error.*/i.test(errMsg)) { 80 | Message({ message: 'WEBUI服务可能已关闭,请重新启动', type: 'info', duration: 3.6 * 1000 }) 81 | } else { 82 | Message({ message: errMsg, type: 'error', duration: 3.6 * 1000 }) 83 | } 84 | return Promise.reject(error) 85 | } 86 | ) 87 | 88 | export default service 89 | 90 | -------------------------------------------------------------------------------- /src/vue/view/404.vue: -------------------------------------------------------------------------------- 1 | 91 | 92 | 103 | 104 | 297 | -------------------------------------------------------------------------------- /src/vue/view/About.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 40 | 41 | 75 | -------------------------------------------------------------------------------- /src/vue/view/Home.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 114 | 115 | 129 | 130 | 214 | -------------------------------------------------------------------------------- /src/vue/view/Test.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 104 | 105 | 121 | -------------------------------------------------------------------------------- /src/vue/view/Type.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 123 | 124 | 226 | 227 | 232 | 233 | --------------------------------------------------------------------------------