├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .travis.deprecated.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── appveyor.yml ├── babel.config.js ├── build └── icons │ ├── 256x256.png │ ├── icon.icns │ └── icon.ico ├── package-lock.json ├── package.json ├── public ├── app.ico ├── favicon.ico └── index.html ├── src ├── background.ts ├── main.ts ├── main │ ├── apis │ │ └── app │ │ │ └── window │ │ │ ├── constants.ts │ │ │ ├── windowList.ts │ │ │ └── windowManager.ts │ ├── events │ │ └── ipcList.ts │ ├── lifeCycle │ │ └── index.ts │ └── utils │ │ ├── beforeOpen.ts │ │ └── updateChecker.ts ├── renderer │ ├── App.vue │ ├── assets │ │ └── logo.png │ ├── components │ │ └── VideoPlayer │ │ │ ├── custom-theme.css │ │ │ ├── index.vue │ │ │ ├── videojs-resume.css │ │ │ └── videojs-resume.ts │ ├── router │ │ └── index.ts │ ├── store │ │ └── index.ts │ ├── style │ │ └── index.scss │ └── views │ │ └── Home │ │ ├── SetList.vue │ │ └── index.vue └── universal │ ├── datastore │ ├── dbChecker.ts │ └── index.ts │ └── types │ ├── electron.d.ts │ ├── extra-vue.d.ts │ ├── shims-module.d.ts │ ├── shims-tsx.d.ts │ ├── types.d.ts │ └── view.d.ts ├── tsconfig.json ├── vue.config.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | globals: { 4 | __static: 'readonly' 5 | }, 6 | env: { 7 | node: true 8 | }, 9 | parser: "vue-eslint-parser", 10 | 'extends': [ 11 | 'plugin:vue/essential', 12 | '@vue/standard', 13 | '@vue/typescript' 14 | ], 15 | 'plugins': ['@typescript-eslint'], 16 | rules: { 17 | 'no-console': process.env.NODE_ENV === 'production' ? 'off' : 'off', 18 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 19 | }, 20 | parserOptions: { 21 | parser: '@typescript-eslint/parser' 22 | } 23 | } -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # main.yml 2 | 3 | # Workflow's name 4 | name: Build 5 | 6 | # Workflow's trigger 7 | on: 8 | push: 9 | branches: 10 | - master 11 | 12 | # Workflow's jobs 13 | jobs: 14 | # job's id 15 | release: 16 | # job's name 17 | name: build and release electron app 18 | 19 | # the type of machine to run the job on 20 | runs-on: ${{ matrix.os }} 21 | 22 | # create a build matrix for jobs 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | os: [ubuntu-latest, macos-10.15] 27 | 28 | # create steps 29 | steps: 30 | # step1: check out repository 31 | - name: Check out git repository 32 | uses: actions/checkout@v2 33 | 34 | # step2: install node env 35 | - name: Install Node.js 36 | uses: actions/setup-node@v2 37 | with: 38 | node-version: ${{ matrix.node }} 39 | 40 | - name: Install system deps 41 | if: matrix.os == 'ubuntu-latest' 42 | run: | 43 | sudo apt-get install --no-install-recommends -y icnsutils graphicsmagick xz-utils 44 | # step3: yarn 45 | - name: Yarn install 46 | run: | 47 | yarn 48 | yarn global add xvfb-maybe 49 | - name: Build & release app 50 | run: | 51 | npm run release 52 | env: 53 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | 25 | #Electron-builder output 26 | /dist_electron -------------------------------------------------------------------------------- /.travis.deprecated.yml: -------------------------------------------------------------------------------- 1 | # Commented sections below can be used to run tests on the CI server 2 | # https://simulatedgreg.gitbooks.io/electron-vue/content/en/testing.html#on-the-subject-of-ci-testing 3 | osx_image: xcode8.3 4 | sudo: required 5 | dist: trusty 6 | language: c 7 | matrix: 8 | include: 9 | - os: osx 10 | - os: linux 11 | env: CC=clang CXX=clang++ npm_config_clang=1 12 | compiler: clang 13 | cache: 14 | directories: 15 | - node_modules 16 | - "$HOME/.electron" 17 | - "$HOME/.cache" 18 | addons: 19 | apt: 20 | packages: 21 | - libgnome-keyring-dev 22 | - icnsutils 23 | before_install: 24 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install git-lfs; fi 25 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install --no-install-recommends -y icnsutils graphicsmagick xz-utils; fi 26 | install: 27 | - nvm install 10 28 | - curl -o- -L https://yarnpkg.com/install.sh | bash 29 | - source ~/.bashrc 30 | - npm install -g xvfb-maybe 31 | - yarn 32 | script: 33 | - npm run release 34 | before_script: 35 | - git lfs pull 36 | branches: 37 | only: 38 | - master 39 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## :tada: 0.1.20 (2021-09-12) 2 | 3 | 4 | ### :sparkles: Features 5 | 6 | * 添加下一集和选集列表按钮 ([ad2de8a](https://github.com/Lingyan000/hikerview-player/commit/ad2de8a)) 7 | 8 | 9 | 10 | ## :tada: 0.1.19 (2021-09-08) 11 | 12 | 13 | ### :sparkles: Features 14 | 15 | * **macos majorization:** macOS 优化 ([98ae5ce](https://github.com/Lingyan000/hikerview-player.git/commit/98ae5ce)) 16 | 17 | 18 | ### :zap: Performance Improvements 19 | 20 | * **video:** donot recreate player when srouce update ([6b6e59c](https://github.com/Lingyan000/hikerview-player.git/commit/6b6e59c)) 21 | 22 | 23 | 24 | ## :tada: 0.1.18 (2021-09-08) 25 | 26 | 27 | ### :sparkles: Features 28 | 29 | * **macos majorization:** macOS 优化 ([98ae5ce](https://github.com/Lingyan000/hikerview-player.git/commit/98ae5ce)) 30 | 31 | 32 | ### :zap: Performance Improvements 33 | 34 | * **video:** donot recreate player when srouce update ([6b6e59c](https://github.com/Lingyan000/hikerview-player.git/commit/6b6e59c)) 35 | 36 | 37 | 38 | ## :tada: 0.1.17-alpha.2 (2021-01-24) 39 | 40 | 41 | 42 | ## :tada: 0.1.17-alpha.1 (2021-01-24) 43 | 44 | 45 | 46 | ## :tada: 0.1.17-alpha.0 (2021-01-21) 47 | 48 | 49 | 50 | ## :tada: 0.1.16 (2021-01-18) 51 | 52 | 53 | 54 | ## :tada: 0.1.16-alpha.4 (2021-01-06) 55 | 56 | 57 | ### :bug: Bug Fixes 58 | 59 | * fix errors caused by ports ([b4e2b87](https://github.com/Lingyan000/hikerview-player/commit/b4e2b87)) 60 | 61 | 62 | 63 | ## :tada: 0.1.16-alpha.3 (2021-01-05) 64 | 65 | 66 | ### :sparkles: Features 67 | 68 | * new function of progress bar ([6d9536f](https://github.com/Lingyan000/hikerview-player/commit/6d9536f)) 69 | 70 | 71 | ### :bug: Bug Fixes 72 | 73 | * 修复成功 Build 的问题 ([b0c08eb](https://github.com/Lingyan000/hikerview-player/commit/b0c08eb)) 74 | 75 | 76 | 77 | ## :tada: 0.1.16-alpha.2 (2021-01-05) 78 | 79 | 80 | ### :sparkles: Features 81 | 82 | * new function of progress bar ([6d9536f](https://github.com/Lingyan000/hikerview-player/commit/6d9536f)) 83 | 84 | 85 | 86 | ## :tada: 0.1.16-alpha.1 (2021-01-04) 87 | 88 | 89 | ### :bug: Bug Fixes 90 | 91 | * bug fixs ([4d02a7f](https://github.com/Lingyan000/hikerview-player/commit/4d02a7f)) 92 | 93 | 94 | 95 | ## :tada: 0.1.16-alpha.0 (2021-01-04) 96 | 97 | 98 | 99 | ## :tada: 0.1.15 (2020-12-30) 100 | 101 | 102 | ### :bug: Bug Fixes 103 | 104 | * fix the problem that disabling CORS is invalid ([915dfbd](https://github.com/Lingyan000/hikerview-player/commit/915dfbd)) 105 | 106 | 107 | 108 | ## :tada: 0.1.15-alpha.2 (2020-12-30) 109 | 110 | 111 | ### :bug: Bug Fixes 112 | 113 | * fix some bugs ([22d3188](https://github.com/Lingyan000/hikerview-player/commit/22d3188)) 114 | 115 | 116 | 117 | ## :tada: 0.1.15-alpha.1 (2020-12-30) 118 | 119 | 120 | ### :sparkles: Features 121 | 122 | * add support for headers ([8bd1a12](https://github.com/Lingyan000/hikerview-player/commit/8bd1a12)) 123 | 124 | 125 | 126 | ## :tada: 0.1.15-alpha.0 (2020-12-29) 127 | 128 | 129 | 130 | ## :tada: 0.1.14 (2020-12-29) 131 | 132 | 133 | ### :bug: Bug Fixes 134 | 135 | * fix. Mac can’t open ([f70aa14](https://github.com/Lingyan000/hikerview-player/commit/f70aa14)) 136 | 137 | 138 | 139 | ## :tada: 0.1.14-alpha.1 (2020-12-28) 140 | 141 | 142 | 143 | ## :tada: 0.1.14-alpha.0 (2020-12-28) 144 | 145 | 146 | 147 | ## :tada: 0.1.13 (2020-12-28) 148 | 149 | 150 | ### :bug: Bug Fixes 151 | 152 | * fix bugs ([6ffb762](https://github.com/Lingyan000/hikerview-player/commit/6ffb762)) 153 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Lingyan000 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 | HikerView LOGO 3 | 4 | # Hikerview Player 5 | 6 | > 海阔视界网页投屏播放器 7 | 8 | [![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/Lingyan000/hikerview-player/Build/master?logo=github)](https://github.com/Lingyan000/hikerview-player/actions?query=workflow%3ABuild) 9 | [![AppVeyor](https://img.shields.io/appveyor/build/Lingyan000/hikerview-player?logo=appveyor&logoColor=ffffff)](https://ci.appveyor.com/project/Lingyan000/hikerview-player) 10 | [![hikerview-player LICENSE](https://img.shields.io/github/license/Lingyan000/hikerview-player)](https://github.com/Lingyan000/hikerview-player/blob/master/LICENSE) 11 | [![GitHub all releases](https://img.shields.io/github/downloads/Lingyan000/hikerview-player/total?logo=github)](https://github.com/Lingyan000/hikerview-player/releases) 12 | [![GitHub releases](https://img.shields.io/github/release/Lingyan000/hikerview-player?style=flat-square&logo=github)](https://github.com/Lingyan000/hikerview-player/releases/latest) 13 |
14 | 15 | ## 特色功能 16 | 17 | - 通过 Electron 打包成软件杜绝视频跨域问题 18 | - 支持解析海阔视界中带 headers 的播放链接 19 | 20 | ## 下载安装 21 | 22 | 点击此处下载 [应用](https://github.com/Lingyan000/hikerview-player/releases)。 23 | 24 | ### Windows 25 | 26 | Windows 用户请下载最新版本的 `exe` 文件。 27 | 28 | ### macOS 29 | 30 | macOS 用户请下载最新版本的 `dmg` 文件。 31 | 32 | ### Linux 33 | 34 | Linux 用户请下载 `AppImage` 文件。 35 | 36 | 37 | ## 应用截图 38 | 39 | ![](https://raw.githubusercontent.com/Lingyan000/photos/master/img/20210106110604.gif) 40 | 41 | ## 开发说明 42 | 43 | > 目前仅针对 Mac、Windows。Linux 平台并未测试。 44 | 45 | 1. 你需要有 Node、Git 环境,了解 npm 的相关知识。 46 | 2. `git clone https://github.com/Lingyan000/hikerview-player.git` 并进入项目。 47 | 3. `yarn` 下载依赖。注意如果你没有 `yarn`,请去 [官网](https://classic.yarnpkg.com/en/docs/install) 下载安装后再使用。 **用 `npm install` 将导致未知错误!** 48 | 4. Mac 需要有 Xcode 环境,Windows 需要有 VS 环境。 49 | 50 | ### 开发模式 51 | 52 | 输入 `npm run electron:serve` 进入开发模式,开发模式具有热重载特性。不过需要注意的是,开发模式不稳定,会有进程崩溃的情况。此时需要: 53 | 54 | ```bash 55 | ctrl+c # 退出开发模式 56 | npm run electron:serve # 重新进入开发模式 57 | ``` 58 | 59 | ### 生产模式 60 | 61 | 如果你需要自行构建,可以 `npm run electron:build` 开始进行构建。构建成功后,会在 `dist_electron` 目录里出现构建成功的相应安装文件。 62 | 63 | **注意**:如果你的网络环境不太好,可能会出现 `electron-builder` 下载 `electron` 二进制文件失败的情况。这个时候需要在 `npm run electron:build` 之前指定一下 `electron` 的源为国内源: 64 | 65 | ```bash 66 | export ELECTRON_MIRROR="https://npm.taobao.org/mirrors/electron/" 67 | # 在 Windows 上,则可以使用 set ELECTRON_MIRROR=https://npm.taobao.org/mirrors/electron/ (无需引号) 68 | npm run electron:build 69 | ``` 70 | 71 | 只需第一次构建的时候指定一下国内源即可。后续构建不需要特地指定。二进制文件下载在 `~/.electron/` 目录下。如果想要更新 `electron` 构建版本,可以删除 `~/.electron/` 目录,然后重新运行上一步,让 `electron-builder `去下载最新的 `electron` 二进制文件。 72 | 73 | ## 鸣谢 74 | 75 | - [Electron](https://github.com/electron/electron) 使一切成为可能 76 | - [Vue.js](https://github.com/vuejs/vue) 渐进式 77 | JavaScript 框架 78 | - [video.js](https://github.com/videojs/video.js) 好用的 H5 播放器 79 | - [PicGo](https://github.com/Molunerfinn/PicGo) 学习了这个项目的开发结构 80 | - [海阔视界](http://haikuoshijie.cn/) 没有它就没有这个项目 81 | 82 | ## License 83 | 84 | [MIT](http://opensource.org/licenses/MIT) 85 | 86 | Copyright (c) 2020 Lingyan000 -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Commented sections below can be used to run tests on the CI server 2 | # https://simulatedgreg.gitbooks.io/electron-vue/content/en/testing.html#on-the-subject-of-ci-testing 3 | version: 0.1.{build} 4 | 5 | branches: 6 | only: 7 | - master 8 | 9 | image: Visual Studio 2017 10 | platform: 11 | - x64 12 | 13 | cache: 14 | - node_modules 15 | - '%APPDATA%\npm-cache' 16 | - '%USERPROFILE%\.electron' 17 | - '%USERPROFILE%\AppData\Local\Yarn\cache' 18 | 19 | init: 20 | - git config --global core.autocrlf input 21 | 22 | install: 23 | - ps: Install-Product node 14 x64 24 | - git reset --hard HEAD 25 | - yarn 26 | - node --version 27 | 28 | build_script: 29 | #- yarn test 30 | - npm run release 31 | 32 | test: off 33 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/cli-plugin-babel/preset'] 3 | } 4 | -------------------------------------------------------------------------------- /build/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lingyan000/hikerview-player/b4b41efc955ad2fa0b27ec53e8b08e8e2cd54771/build/icons/256x256.png -------------------------------------------------------------------------------- /build/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lingyan000/hikerview-player/b4b41efc955ad2fa0b27ec53e8b08e8e2cd54771/build/icons/icon.icns -------------------------------------------------------------------------------- /build/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lingyan000/hikerview-player/b4b41efc955ad2fa0b27ec53e8b08e8e2cd54771/build/icons/icon.ico -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hikerview-player", 3 | "version": "0.1.20", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint", 9 | "electron:build": "vue-cli-service electron:build", 10 | "electron:serve": "vue-cli-service electron:serve", 11 | "postinstall": "electron-builder install-app-deps", 12 | "postuninstall": "electron-builder install-app-deps", 13 | "cz": "git-cz", 14 | "bump": "bump-version", 15 | "release": "vue-cli-service electron:build --publish always" 16 | }, 17 | "main": "background.js", 18 | "husky": { 19 | "hooks": { 20 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 21 | } 22 | }, 23 | "config": { 24 | "commitizen": { 25 | "path": "./node_modules/cz-customizable" 26 | }, 27 | "cz-customizable": { 28 | "config": "./node_modules/@picgo/bump-version/.cz-config.js" 29 | } 30 | }, 31 | "commitlint": { 32 | "extends": [ 33 | "./node_modules/@picgo/bump-version/commitlint-picgo" 34 | ] 35 | }, 36 | "dependencies": { 37 | "axios": "^0.21.1", 38 | "core-js": "^3.8.1", 39 | "dayjs": "^1.9.7", 40 | "element-ui": "^2.14.1", 41 | "fix-path": "^3.0.0", 42 | "fs-extra": "^9.0.1", 43 | "lodash-id": "^0.14.0", 44 | "lowdb": "^1.0.0", 45 | "mux.js": "^5.7.0", 46 | "video.js": "7.7.5", 47 | "videojs-contrib-hls": "^5.15.0", 48 | "videojs-hotkeys": "^0.2.27", 49 | "vue": "^2.6.11", 50 | "vue-class-component": "^7.2.3", 51 | "vue-property-decorator": "^8.4.2", 52 | "vue-router": "^3.2.0", 53 | "vuex": "^3.4.0" 54 | }, 55 | "devDependencies": { 56 | "@picgo/bump-version": "^1.1.0", 57 | "@types/electron-devtools-installer": "^2.2.0", 58 | "@types/fs-extra": "8.0.1", 59 | "@types/lowdb": "^1.0.9", 60 | "@types/node": "^14.14.16", 61 | "@types/semver": "^7.3.4", 62 | "@types/video.js": "^7.3.11", 63 | "@typescript-eslint/eslint-plugin": "^2.33.0", 64 | "@typescript-eslint/parser": "^2.33.0", 65 | "@vue/cli-plugin-babel": "~4.5.0", 66 | "@vue/cli-plugin-eslint": "~4.5.0", 67 | "@vue/cli-plugin-router": "~4.5.0", 68 | "@vue/cli-plugin-typescript": "~4.5.0", 69 | "@vue/cli-plugin-vuex": "~4.5.0", 70 | "@vue/cli-service": "~4.5.0", 71 | "@vue/eslint-config-standard": "4.0.0", 72 | "@vue/eslint-config-typescript": "4.0.0", 73 | "babel-eslint": "^10.1.0", 74 | "electron": "9.3.1", 75 | "electron-devtools-installer": "^3.1.0", 76 | "eslint": "^6.7.2", 77 | "eslint-plugin-prettier": "^3.1.3", 78 | "eslint-plugin-vue": "^6.2.2", 79 | "node-sass": "^4.12.0", 80 | "prettier": "^1.19.1", 81 | "sass-loader": "^8.0.2", 82 | "typescript": "3.9.3", 83 | "vue-cli-plugin-electron-builder": "~2.0.0-rc.5", 84 | "vue-template-compiler": "^2.6.11" 85 | }, 86 | "eslintConfig": { 87 | "root": true, 88 | "env": { 89 | "node": true 90 | }, 91 | "extends": [ 92 | "plugin:vue/essential", 93 | "eslint:recommended", 94 | "@vue/prettier", 95 | "@vue/typescript" 96 | ], 97 | "parserOptions": { 98 | "parser": "@typescript-eslint/parser" 99 | }, 100 | "rules": {} 101 | }, 102 | "browserslist": [ 103 | "> 1%", 104 | "last 2 versions", 105 | "not dead" 106 | ], 107 | "resolutions": { 108 | "@types/node": "12.0.2" 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /public/app.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lingyan000/hikerview-player/b4b41efc955ad2fa0b27ec53e8b08e8e2cd54771/public/app.ico -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lingyan000/hikerview-player/b4b41efc955ad2fa0b27ec53e8b08e8e2cd54771/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 海阔视界播放器 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/background.ts: -------------------------------------------------------------------------------- 1 | import { bootstrap } from '~/main/lifeCycle' 2 | 3 | bootstrap.launchApp() 4 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './renderer/App.vue' 3 | import router from './renderer/router' 4 | import db from '#/datastore' 5 | import store from './renderer/store' 6 | import '@/style/index.scss' 7 | import ElementUI from 'element-ui' 8 | import 'element-ui/lib/theme-chalk/index.css' 9 | 10 | Vue.config.productionTip = false 11 | Vue.prototype.$db = db 12 | 13 | Vue.use(ElementUI) 14 | 15 | new Vue({ 16 | router, 17 | store, 18 | render: h => h(App) 19 | }).$mount('#app') 20 | -------------------------------------------------------------------------------- /src/main/apis/app/window/constants.ts: -------------------------------------------------------------------------------- 1 | export enum IWindowList { 2 | MAIN_WINDOW = 'MAIN_WINDOW', 3 | } 4 | 5 | const isDevelopment = process.env.NODE_ENV !== 'production' 6 | 7 | export const MAIN_WINDOW_URL = isDevelopment 8 | ? (process.env.WEBPACK_DEV_SERVER_URL as string) 9 | : `hvp://./index.html` 10 | -------------------------------------------------------------------------------- /src/main/apis/app/window/windowList.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IWindowList, 3 | MAIN_WINDOW_URL 4 | } from './constants' 5 | import { IWindowListItem } from '#/types/electron' 6 | 7 | const windowList = new Map() 8 | 9 | windowList.set(IWindowList.MAIN_WINDOW, { 10 | isValid: process.platform !== 'linux', 11 | multiple: false, 12 | options () { 13 | const options: IBrowserWindowOptions = { 14 | height: 600, 15 | width: 800, 16 | show: true, 17 | frame: true, 18 | center: true, 19 | fullscreenable: true, 20 | resizable: true, 21 | title: '海阔视界播放器', 22 | // vibrancy: 'ultra-dark', 23 | // transparent: true, 24 | // titleBarStyle: 'hidden', 25 | webPreferences: { 26 | backgroundThrottling: false, 27 | nodeIntegration: true, 28 | nodeIntegrationInWorker: true, 29 | webSecurity: false 30 | } 31 | } 32 | if (process.platform !== 'darwin') { 33 | options.backgroundColor = '#3f3c37' 34 | options.transparent = false 35 | options.icon = `${__static}/app.ico` 36 | } 37 | return options 38 | }, 39 | callback (window) { 40 | if (process.env.WEBPACK_DEV_SERVER_URL && !process.env.IS_TEST) window.webContents.openDevTools() 41 | window.loadURL(MAIN_WINDOW_URL) 42 | } 43 | }) 44 | 45 | export default windowList 46 | -------------------------------------------------------------------------------- /src/main/apis/app/window/windowManager.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BrowserWindow 3 | } from 'electron' 4 | import { IWindowManager, IWindowListItem } from '#/types/electron' 5 | import windowList from './windowList' 6 | import { 7 | IWindowList 8 | } from './constants' 9 | 10 | class WindowManager implements IWindowManager { 11 | private windowMap: Map = new Map() 12 | private windowIdMap: Map = new Map() 13 | create (name: IWindowList) { 14 | const windowConfig: IWindowListItem = windowList.get(name)! 15 | if (windowConfig.isValid) { 16 | if (!windowConfig.multiple) { 17 | if (this.has(name)) return this.windowMap.get(name)! 18 | } 19 | const window = new BrowserWindow(windowConfig.options()) 20 | const id = window.id 21 | if (windowConfig.multiple) { 22 | this.windowMap.set(`${name}_${window.id}`, window) 23 | this.windowIdMap.set(window.id, `${name}_${window.id}`) 24 | } else { 25 | this.windowMap.set(name, window) 26 | this.windowIdMap.set(window.id, name) 27 | } 28 | windowConfig.callback(window, this) 29 | window.on('close', () => { 30 | this.deleteById(id) 31 | }) 32 | return window 33 | } else { 34 | return null 35 | } 36 | } 37 | get (name: IWindowList) { 38 | if (this.has(name)) { 39 | return this.windowMap.get(name)! 40 | } else { 41 | const window = this.create(name) 42 | return window 43 | } 44 | } 45 | has (name: IWindowList) { 46 | return this.windowMap.has(name) 47 | } 48 | 49 | deleteById = (id: number) => { 50 | const name = this.windowIdMap.get(id) 51 | if (name) { 52 | this.windowMap.delete(name) 53 | this.windowIdMap.delete(id) 54 | } 55 | } 56 | getAvailableWindow () { 57 | const trayWindow = this.windowMap.get(IWindowList.MAIN_WINDOW) 58 | return trayWindow! 59 | } 60 | } 61 | 62 | export default new WindowManager() 63 | -------------------------------------------------------------------------------- /src/main/events/ipcList.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Filter, 3 | ipcMain, 4 | IpcMainEvent, 5 | session, 6 | Notification 7 | } from 'electron' 8 | 9 | export default { 10 | listen () { 11 | ipcMain.on( 12 | 'uploadRequestHeaders', 13 | async (evt: IpcMainEvent, filter: Filter, headers) => { 14 | try { 15 | session.defaultSession.webRequest.onBeforeSendHeaders(filter, (details, callback) => { 16 | Object.keys(headers).forEach((key) => { 17 | details.requestHeaders[key] = headers[key].replace(/;;/g, ';').replace(/%%/g, ';').split('.js:')[0] 18 | }) 19 | let requestHeaders = { requestHeaders: details.requestHeaders } 20 | callback(requestHeaders) 21 | }) 22 | } catch (e) { 23 | const notification = new Notification({ 24 | title: '错误', 25 | body: '糟糕...发生了一些错误,可能是 headers 有误' 26 | }) 27 | notification.show() 28 | throw new Error(e) 29 | } 30 | } 31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/lifeCycle/index.ts: -------------------------------------------------------------------------------- 1 | import { app, globalShortcut, protocol, Menu } from 'electron' 2 | import { 3 | createProtocol 4 | } from 'vue-cli-plugin-electron-builder/lib' 5 | import beforeOpen from '~/main/utils/beforeOpen' 6 | import fixPath from 'fix-path' 7 | import ipcList from '~/main/events/ipcList' 8 | import { IWindowList } from 'apis/app/window/constants' 9 | import windowManager from 'apis/app/window/windowManager' 10 | import updateChecker from '~/main/utils/updateChecker' 11 | import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer' 12 | import db from '#/datastore' 13 | 14 | const isDevelopment = process.env.NODE_ENV !== 'production' 15 | const isMac = process.platform === 'darwin' 16 | 17 | class LifeCycle { 18 | private beforeReady () { 19 | protocol.registerSchemesAsPrivileged([ 20 | { scheme: 'hvp', privileges: { secure: true, standard: true } } 21 | ]) 22 | app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors') 23 | // fix the $PATH in macOS 24 | fixPath() 25 | beforeOpen() 26 | ipcList.listen() 27 | } 28 | private onReady () { 29 | app.on('ready', async () => { 30 | createProtocol('hvp') 31 | let menu = Menu.buildFromTemplate(isMac ? [{ 32 | label: app.name, 33 | submenu: [{ role: 'about' }, { role: 'quit' }] 34 | }] : []) 35 | Menu.setApplicationMenu(menu) 36 | if (isDevelopment && !process.env.IS_TEST) { 37 | // Install Vue Devtools 38 | try { 39 | await installExtension(VUEJS_DEVTOOLS) 40 | } catch (e) { 41 | console.error('Vue Devtools failed to install:', e.toString()) 42 | } 43 | } 44 | windowManager.create(IWindowList.MAIN_WINDOW) 45 | db.set('needReload', false) 46 | updateChecker() 47 | }) 48 | } 49 | private onRunning () { 50 | app.on('activate', () => { 51 | createProtocol('hvp') 52 | if (!windowManager.has(IWindowList.MAIN_WINDOW)) { 53 | windowManager.create(IWindowList.MAIN_WINDOW) 54 | } 55 | }) 56 | app.setLoginItemSettings({ 57 | openAtLogin: db.get('settings.autoStart') || false 58 | }) 59 | if (process.platform === 'win32') { 60 | app.setAppUserModelId('com.Lingyan000.hikerviewplayer') 61 | } 62 | 63 | if ( 64 | process.env.XDG_CURRENT_DESKTOP && 65 | process.env.XDG_CURRENT_DESKTOP.includes('Unity') 66 | ) { 67 | process.env.XDG_CURRENT_DESKTOP = 'Unity' 68 | } 69 | } 70 | private onQuit () { 71 | app.on('window-all-closed', () => { 72 | if (!isMac) { 73 | app.quit() 74 | } 75 | }) 76 | 77 | app.on('will-quit', () => { 78 | globalShortcut.unregisterAll() 79 | }) 80 | // Exit cleanly on request from parent process in development mode. 81 | if (isDevelopment) { 82 | if (process.platform === 'win32') { 83 | process.on('message', (data) => { 84 | if (data === 'graceful-exit') { 85 | app.quit() 86 | } 87 | }) 88 | } else { 89 | process.on('SIGTERM', () => { 90 | app.quit() 91 | }) 92 | } 93 | } 94 | } 95 | launchApp () { 96 | const gotTheLock = app.requestSingleInstanceLock() 97 | if (!gotTheLock) { 98 | app.quit() 99 | } else { 100 | this.beforeReady() 101 | this.onReady() 102 | this.onRunning() 103 | this.onQuit() 104 | } 105 | } 106 | } 107 | 108 | const bootstrap = new LifeCycle() 109 | 110 | export { bootstrap } 111 | -------------------------------------------------------------------------------- /src/main/utils/beforeOpen.ts: -------------------------------------------------------------------------------- 1 | import { remote, app } from 'electron' 2 | import pkg from 'root/package.json' 3 | 4 | const APP = process.type === 'renderer' ? remote.app : app 5 | 6 | function beforeOpen () { 7 | injectHvpVersion() 8 | } 9 | 10 | function injectHvpVersion () { 11 | global.HVP_GUI_VERSION = pkg.version 12 | } 13 | 14 | export default beforeOpen 15 | -------------------------------------------------------------------------------- /src/main/utils/updateChecker.ts: -------------------------------------------------------------------------------- 1 | import { dialog, shell } from 'electron' 2 | import db from '#/datastore' 3 | import axios from 'axios' 4 | import pkg from 'root/package.json' 5 | import { lt } from 'semver' 6 | const version = pkg.version 7 | const releaseUrl = 8 | 'https://api.github.com/repos/Lingyan000/hikerview-player/releases/latest' 9 | const releaseUrlBackup = 10 | 'https://cdn.jsdelivr.net/gh/Lingyan000/hikerview-player@latest/package.json' 11 | const downloadUrl = 12 | 'https://github.com/Lingyan000/hikerview-player/releases/latest' 13 | 14 | const checkVersion = async () => { 15 | let showTip = db.get('settings.showUpdateTip') 16 | if (showTip === undefined) { 17 | db.set('settings.showUpdateTip', true) 18 | showTip = true 19 | } 20 | if (showTip) { 21 | let res: any 22 | try { 23 | res = await axios.get(releaseUrl).catch(async () => { 24 | const result = await axios.get(releaseUrlBackup) 25 | return result 26 | }) 27 | } catch (err) { 28 | console.log(err) 29 | } 30 | if (res.status === 200) { 31 | const latest = res.data.version || res.data.name 32 | const result = compareVersion2Update(version, latest) 33 | if (result) { 34 | dialog 35 | .showMessageBox({ 36 | type: 'info', 37 | title: '发现新版本', 38 | buttons: ['Yes', 'No'], 39 | message: `发现新版本${latest},更新了很多功能,是否去下载最新的版本?`, 40 | checkboxLabel: '以后不再提醒', 41 | checkboxChecked: false 42 | }) 43 | .then((res) => { 44 | if (res.response === 0) { 45 | // if selected yes 46 | shell.openExternal(downloadUrl) 47 | } 48 | db.set('settings.showUpdateTip', !res.checkboxChecked) 49 | }) 50 | } 51 | } else { 52 | return false 53 | } 54 | } else { 55 | return false 56 | } 57 | } 58 | 59 | // if true -> update else return false 60 | const compareVersion2Update = (current: string, latest: string) => { 61 | try { 62 | if (latest.includes('beta')) { 63 | const isCheckBetaUpdate = db.get('settings.checkBetaUpdate') !== false 64 | if (!isCheckBetaUpdate) { 65 | return false 66 | } 67 | } 68 | return lt(current, latest) 69 | } catch (e) { 70 | return false 71 | } 72 | } 73 | 74 | export default checkVersion 75 | -------------------------------------------------------------------------------- /src/renderer/App.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/renderer/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lingyan000/hikerview-player/b4b41efc955ad2fa0b27ec53e8b08e8e2cd54771/src/renderer/assets/logo.png -------------------------------------------------------------------------------- /src/renderer/components/VideoPlayer/custom-theme.css: -------------------------------------------------------------------------------- 1 | .vjs-custom-skin > .video-js { 2 | width: 100%; 3 | font-family: "PingFang SC","Helvetica Neue","Hiragino Sans GB","Segoe UI","Microsoft YaHei","微软雅黑",sans-serif; 4 | } 5 | 6 | .vjs-custom-skin > .video-js .vjs-menu-button-inline.vjs-slider-active,.vjs-custom-skin > .video-js .vjs-menu-button-inline:focus,.vjs-custom-skin > .video-js .vjs-menu-button-inline:hover,.video-js.vjs-no-flex .vjs-menu-button-inline { 7 | width: 10em 8 | } 9 | 10 | .vjs-custom-skin > .video-js .vjs-controls-disabled .vjs-big-play-button { 11 | display: none!important 12 | } 13 | 14 | .vjs-custom-skin > .video-js .vjs-control { 15 | width: 3em 16 | } 17 | 18 | .vjs-custom-skin > .video-js .vjs-control.vjs-live-control{ 19 | width: auto; 20 | padding-left: .5em; 21 | letter-spacing: .1em; 22 | } 23 | 24 | .vjs-custom-skin > .video-js .vjs-menu-button-inline:before { 25 | width: 1.5em 26 | } 27 | 28 | .vjs-menu-button-inline .vjs-menu { 29 | left: 3em 30 | } 31 | 32 | .vjs-paused.vjs-has-started.vjs-custom-skin > .video-js .vjs-big-play-button,.video-js.vjs-ended .vjs-big-play-button,.video-js.vjs-paused .vjs-big-play-button { 33 | display: block 34 | } 35 | 36 | .vjs-custom-skin > .video-js .vjs-load-progress div,.vjs-seeking .vjs-big-play-button,.vjs-waiting .vjs-big-play-button { 37 | display: none!important 38 | } 39 | 40 | .vjs-custom-skin > .video-js .vjs-mouse-display:after,.vjs-custom-skin > .video-js .vjs-play-progress:after { 41 | padding: 0 .4em .3em 42 | } 43 | 44 | .video-js.vjs-ended .vjs-loading-spinner { 45 | display: none; 46 | } 47 | 48 | .video-js.vjs-ended .vjs-big-play-button { 49 | display: block !important; 50 | } 51 | 52 | .video-js.vjs-ended .vjs-big-play-button,.video-js.vjs-paused .vjs-big-play-button,.vjs-paused.vjs-has-started.vjs-custom-skin > .video-js .vjs-big-play-button { 53 | display: block 54 | } 55 | 56 | .vjs-custom-skin > .video-js .vjs-big-play-button { 57 | top: 50%; 58 | left: 50%; 59 | margin-left: -1.5em; 60 | margin-top: -1em 61 | } 62 | 63 | .vjs-custom-skin > .video-js .vjs-big-play-button { 64 | background-color: rgba(0,0,0,0.45); 65 | font-size: 3.5em; 66 | /*border-radius: 50%;*/ 67 | height: 2em !important; 68 | line-height: 2em !important; 69 | margin-top: -1em !important 70 | } 71 | 72 | .video-js:hover .vjs-big-play-button,.vjs-custom-skin > .video-js .vjs-big-play-button:focus,.vjs-custom-skin > .video-js .vjs-big-play-button:active { 73 | background-color: rgba(36,131,213,0.9) 74 | } 75 | 76 | .vjs-custom-skin > .video-js .vjs-loading-spinner { 77 | border-color: rgba(36,131,213,0.8) 78 | } 79 | 80 | .vjs-custom-skin > .video-js .vjs-control-bar2 { 81 | background-color: #000000 82 | } 83 | 84 | .vjs-custom-skin > .video-js .vjs-control-bar { 85 | /*background-color: rgba(0,0,0,0.3) !important;*/ 86 | color: #ffffff; 87 | font-size: 14px 88 | } 89 | 90 | .vjs-custom-skin > .video-js .vjs-play-progress,.vjs-custom-skin > .video-js .vjs-volume-level { 91 | background-color: #2483d5 92 | } 93 | 94 | .vjs-custom-skin > .video-js .vjs-play-progress:before { 95 | top: -0.3em; 96 | } 97 | 98 | .vjs-custom-skin > .video-js .vjs-progress-control:hover .vjs-progress-holder { 99 | font-size: 1.3em; 100 | } 101 | 102 | .vjs-menu-button-popup.vjs-volume-menu-button-vertical .vjs-menu { 103 | left: 0em; 104 | } 105 | 106 | .vjs-custom-skin > .video-js .vjs-menu li { 107 | padding: 0; 108 | line-height: 2em; 109 | font-size: 1.1em; 110 | font-family: "PingFang SC","Helvetica Neue","Hiragino Sans GB","Segoe UI","Microsoft YaHei","微软雅黑",sans-serif; 111 | } 112 | 113 | .vjs-custom-skin > .video-js .vjs-time-tooltip, 114 | .vjs-custom-skin > .video-js .vjs-mouse-display:after, 115 | .vjs-custom-skin > .video-js .vjs-play-progress:after { 116 | border-radius: 0; 117 | font-size: 1em; 118 | padding: 0; 119 | width: 3em; 120 | height: 1.5em; 121 | line-height: 1.5em; 122 | top: -3em; 123 | } 124 | 125 | .vjs-custom-skin > .video-js .vjs-menu-button-popup .vjs-menu { 126 | width: 5em; 127 | left: -1em; 128 | } 129 | 130 | .vjs-custom-skin > .video-js .vjs-menu-button-popup.vjs-volume-menu-button-vertical .vjs-menu { 131 | left: 0; 132 | } 133 | 134 | .vjs-custom-skin > .video-js .vjs-control-bar .vjs-resolution-button .vjs-menu { 135 | /*order: 4;*/ 136 | } 137 | 138 | /*排序顺序*/ 139 | .vjs-custom-skin > .video-js .vjs-control-bar .vjs-play-control { 140 | order: 0; 141 | } 142 | 143 | .vjs-custom-skin > .video-js .vjs-control-bar .vjs-time-control { 144 | min-width: 1em; 145 | padding: 0; 146 | margin: 0 .1em; 147 | text-align: center; 148 | display: block; 149 | order: 1; 150 | } 151 | 152 | .vjs-custom-skin > .video-js .vjs-control-bar .vjs-playback-rate .vjs-playback-rate-value{ 153 | font-size: 1.2em; 154 | line-height: 2.4; 155 | } 156 | 157 | .vjs-custom-skin > .video-js .vjs-progress-control.vjs-control { 158 | order: 2; 159 | } 160 | 161 | .vjs-custom-skin > .video-js .vjs-control-bar .vjs-volume-menu-button { 162 | order: 3; 163 | } 164 | 165 | .vjs-custom-skin > .video-js .vjs-control-bar .vjs-resolution-button { 166 | order: 4; 167 | } 168 | 169 | .vjs-custom-skin > .video-js .vjs-control-bar .vjs-resolution-button .vjs-resolution-button-label { 170 | display: block; 171 | line-height: 3em; 172 | } 173 | 174 | .vjs-custom-skin > .video-js .vjs-control-bar .vjs-playback-rate { 175 | order: 5; 176 | } 177 | 178 | .vjs-custom-skin > .video-js .vjs-control-bar .vjs-fullscreen-control { 179 | order: 6; 180 | } -------------------------------------------------------------------------------- /src/renderer/components/VideoPlayer/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 307 | 308 | 337 | -------------------------------------------------------------------------------- /src/renderer/components/VideoPlayer/videojs-resume.css: -------------------------------------------------------------------------------- 1 | .vjs-default-skin .vjs-resume-modal .vjs-resume-modal-buttons { 2 | position: relative; 3 | top: 50%; 4 | transform: translateY(-50%); 5 | background-color: #111; 6 | color: #fff; 7 | margin: 0 auto; 8 | padding: 10px 0; 9 | font-size: 14px; 10 | width: 225px; 11 | border-radius: 10px; 12 | text-align: center; 13 | } 14 | 15 | .vjs-default-skin .vjs-resume-modal .vjs-resume-modal-buttons p { 16 | margin: 0 0 10px 0; 17 | } 18 | 19 | .vjs-default-skin .vjs-resume-modal .vjs-resume-modal-buttons button { 20 | color: #fff; 21 | padding: 10px; 22 | background-color: #42a5f5; 23 | border-radius: 5px; 24 | margin: 5px; 25 | font-size: 12px; 26 | cursor: pointer; 27 | } 28 | 29 | .vjs-default-skin .vjs-resume-modal .vjs-resume-modal-buttons button:hover { 30 | background-color: #90caf9; 31 | } 32 | -------------------------------------------------------------------------------- /src/renderer/components/VideoPlayer/videojs-resume.ts: -------------------------------------------------------------------------------- 1 | import videojs from 'video.js' 2 | import db from '#/datastore' 3 | 4 | const Button = videojs.getComponent('Button') 5 | const Component = videojs.getComponent('Component') 6 | const ModalDialog = videojs.getComponent('ModalDialog') 7 | 8 | class ResumeButton extends Button { 9 | resumeFromTime: any 10 | player_: any 11 | player: any 12 | controlText_: string | undefined 13 | options_: any 14 | constructor (player: any, options: any) { 15 | super(player, options) 16 | this.resumeFromTime = options.resumeFromTime 17 | this.player = player 18 | } 19 | 20 | buildCSSClass () { 21 | return 'vjs-resume' 22 | } 23 | 24 | createEl () { 25 | return super.createEl('button', { 26 | innerHTML: `${this.options_.buttonText}` 27 | }) 28 | } 29 | 30 | handleClick () { 31 | this.player_.resumeModal.close() 32 | this.player.currentTime(this.resumeFromTime) 33 | this.player.play() 34 | this.player.trigger('resumevideo') 35 | } 36 | 37 | handleKeyPress (event:any) { 38 | // Check for space bar (32) or enter (13) keys 39 | if (event.which === 32 || event.which === 13) { 40 | if (this.player.paused()) { 41 | this.player.play() 42 | } else { 43 | this.player.pause() 44 | } 45 | event.preventDefault() 46 | } 47 | } 48 | } 49 | 50 | ResumeButton.prototype.controlText_ = 'Resume' 51 | 52 | class ResumeCancelButton extends Button { 53 | player_: any 54 | options_: any 55 | buildCSSClass () { 56 | return 'vjs-no-resume' 57 | } 58 | 59 | createEl () { 60 | return super.createEl('button', { 61 | innerHTML: `${this.options_.buttonText}` 62 | }) 63 | } 64 | 65 | handleClick () { 66 | this.player_.resumeModal.close() 67 | db.get(`resume[${this.options_.key.replace(/\./g, '_')}]`) 68 | } 69 | } 70 | ResumeButton.prototype.controlText_ = 'No Thanks' 71 | 72 | class ModalButtons extends Component { 73 | player_: any 74 | options_: any 75 | constructor (player: any, options: any) { 76 | super(player, options) 77 | this.addChild('ResumeButton', { 78 | buttonText: options.resumeButtonText, 79 | resumeFromTime: options.resumeFromTime 80 | }) 81 | this.addChild('ResumeCancelButton', { 82 | buttonText: options.cancelButtonText, 83 | key: options.key 84 | }) 85 | } 86 | 87 | createEl () { 88 | return super.createEl('div', { 89 | className: 'vjs-resume-modal-buttons', 90 | innerHTML: ` 91 |

${this.options_.title}

92 | ` 93 | }) 94 | } 95 | } 96 | 97 | class ResumeModal extends ModalDialog { 98 | player_: any 99 | constructor (player: any, options: any) { 100 | super(player, options) 101 | this.player_.resumeModal = this 102 | this.open() 103 | this.addChild('ModalButtons', { 104 | title: options.title, 105 | resumeButtonText: options.resumeButtonText, 106 | cancelButtonText: options.cancelButtonText, 107 | resumeFromTime: options.resumeFromTime, 108 | key: options.key 109 | }) 110 | } 111 | 112 | buildCSSClass () { 113 | return `vjs-resume-modal ${super.buildCSSClass()}` 114 | } 115 | } 116 | videojs.registerComponent('ResumeButton', ResumeButton) 117 | videojs.registerComponent('ResumeCancelButton', ResumeCancelButton) 118 | videojs.registerComponent('ModalButtons', ModalButtons) 119 | videojs.registerComponent('ResumeModal', ResumeModal) 120 | 121 | const Resume = function (this: any, options: any) { 122 | let videoId = options.uuid 123 | let title = options.title || 'Resume from where you left off?' 124 | let resumeButtonText = options.resumeButtonText || 'Resume' 125 | let cancelButtonText = options.cancelButtonText || 'No Thanks' 126 | let playbackOffset = options.playbackOffset || 0 127 | let key = 'videojs-resume:' + videoId 128 | 129 | this.on('timeupdate', function () { 130 | // @ts-ignore 131 | db.set(`resume[${key.replace(/\./g, '_')}]`, this.currentTime()) 132 | }) 133 | 134 | this.on('ended', function () { 135 | db.read().unset(`resume[${key.replace(/\./g, '_')}]`).write() 136 | }) 137 | 138 | this.ready(function () { 139 | let resumeFromTime:number = db.get(`resume.${key.replace(/\./g, '_')}`) as number 140 | 141 | if (resumeFromTime) { 142 | if (resumeFromTime >= 5) { 143 | resumeFromTime -= playbackOffset 144 | } 145 | if (resumeFromTime <= 0) { 146 | resumeFromTime = 0 147 | } 148 | // @ts-ignore 149 | this.addChild('ResumeModal', { 150 | title, 151 | resumeButtonText, 152 | cancelButtonText, 153 | resumeFromTime, 154 | key 155 | }) 156 | } 157 | }) 158 | } 159 | 160 | videojs.plugin('Resume', Resume) 161 | -------------------------------------------------------------------------------- /src/renderer/router/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | 4 | Vue.use(VueRouter) 5 | 6 | const routes = [ 7 | { 8 | path: '/', 9 | name: 'Home', 10 | component: () => import(/* webpackChunkName: "tray" */ '@/views/Home/index.vue') 11 | } 12 | ] 13 | 14 | const router = new VueRouter({ 15 | mode: 'hash', 16 | base: process.env.BASE_URL, 17 | routes 18 | }) 19 | 20 | export default router 21 | -------------------------------------------------------------------------------- /src/renderer/store/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | Vue.use(Vuex) 5 | 6 | export default new Vuex.Store({ 7 | state: {}, 8 | mutations: {}, 9 | actions: {}, 10 | modules: {} 11 | }) 12 | -------------------------------------------------------------------------------- /src/renderer/style/index.scss: -------------------------------------------------------------------------------- 1 | body { 2 | -moz-osx-font-smoothing: grayscale; 3 | -webkit-font-smoothing: antialiased; 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | html { 9 | box-sizing: border-box; 10 | } 11 | 12 | :focus { 13 | outline: none !important; 14 | } 15 | 16 | @font-face { 17 | font-family: 'iconfont'; /* Project id 2806478 */ 18 | src: url('//at.alicdn.com/t/font_2806478_g43exceyrs5.woff2?t=1631364862905') format('woff2'), 19 | url('//at.alicdn.com/t/font_2806478_g43exceyrs5.woff?t=1631364862905') format('woff'), 20 | url('//at.alicdn.com/t/font_2806478_g43exceyrs5.ttf?t=1631364862905') format('truetype'); 21 | } 22 | -------------------------------------------------------------------------------- /src/renderer/views/Home/SetList.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 87 | 88 | 115 | -------------------------------------------------------------------------------- /src/renderer/views/Home/index.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 189 | 190 | 200 | -------------------------------------------------------------------------------- /src/universal/datastore/dbChecker.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import path from 'path' 3 | import { app } from 'electron' 4 | import dayjs from 'dayjs' 5 | 6 | const errorMsg = { 7 | broken: 'HVP 配置文件损坏,已经恢复为默认配置', 8 | brokenButBackup: 'HVP 配置文件损坏,已经恢复为备份配置' 9 | } 10 | 11 | function dbChecker () { 12 | if (process.type !== 'renderer') { 13 | // @ts-ignore 14 | if (!global.notificationList) global.notificationList = [] 15 | const STORE_PATH = app.getPath('userData') 16 | const configFilePath = path.join(STORE_PATH, 'data.json') 17 | const configFileBackupPath = path.join(STORE_PATH, 'data.bak.json') 18 | if (!fs.existsSync(configFilePath)) { 19 | return 20 | } 21 | let configFile = '' 22 | let optionsTpl = { 23 | title: '注意', 24 | body: '' 25 | } 26 | try { 27 | configFile = fs.readFileSync(configFilePath, { encoding: 'utf-8' }) 28 | JSON.parse(configFile) 29 | } catch (e) { 30 | fs.unlinkSync(configFilePath) 31 | if (fs.existsSync(configFileBackupPath)) { 32 | try { 33 | configFile = fs.readFileSync(configFileBackupPath, { 34 | encoding: 'utf-8' 35 | }) 36 | JSON.parse(configFile) 37 | fs.writeFileSync(configFilePath, configFile, { encoding: 'utf-8' }) 38 | const stats = fs.statSync(configFileBackupPath) 39 | optionsTpl.body = `${errorMsg.brokenButBackup}\n备份文件版本:${dayjs( 40 | stats.mtime 41 | ).format('YYYY-MM-DD HH:mm:ss')}` 42 | // @ts-ignore 43 | global.notificationList.push(optionsTpl) 44 | return 45 | } catch (e) { 46 | optionsTpl.body = errorMsg.broken 47 | // @ts-ignore 48 | global.notificationList.push(optionsTpl) 49 | return 50 | } 51 | } 52 | optionsTpl.body = errorMsg.broken 53 | // @ts-ignore 54 | global.notificationList.push(optionsTpl) 55 | return 56 | } 57 | fs.writeFileSync(configFileBackupPath, configFile, { encoding: 'utf-8' }) 58 | } 59 | } 60 | 61 | export { dbChecker } 62 | -------------------------------------------------------------------------------- /src/universal/datastore/index.ts: -------------------------------------------------------------------------------- 1 | import Datastore from 'lowdb' 2 | // @ts-ignore 3 | import LodashId from 'lodash-id' 4 | import FileSync from 'lowdb/adapters/FileSync' 5 | import path from 'path' 6 | import fs from 'fs-extra' 7 | import { remote, app } from 'electron' 8 | import { dbChecker } from './dbChecker' 9 | 10 | const APP = process.type === 'renderer' ? remote.app : app 11 | const STORE_PATH = APP.getPath('userData') 12 | 13 | if (process.type !== 'renderer') { 14 | if (!fs.pathExistsSync(STORE_PATH)) { 15 | fs.mkdirpSync(STORE_PATH) 16 | } 17 | } 18 | 19 | dbChecker() 20 | 21 | class DB { 22 | private db: Datastore.LowdbSync; 23 | constructor () { 24 | const adapter = new FileSync(path.join(STORE_PATH, '/data.json')) 25 | 26 | this.db = Datastore(adapter) 27 | this.db._.mixin(LodashId) 28 | } 29 | read () { 30 | return this.db.read() 31 | } 32 | get (key = '') { 33 | return this.read() 34 | .get(key) 35 | .value() 36 | } 37 | set (key: string, value: any) { 38 | return this.read() 39 | .set(key, value) 40 | .write() 41 | } 42 | has (key: string) { 43 | return this.read() 44 | .has(key) 45 | .value() 46 | } 47 | insert (key: string, value: any): void { 48 | return ( 49 | this.read() 50 | .get(key) 51 | // @ts-ignore 52 | .insert(value) 53 | .write() 54 | ) 55 | } 56 | unset (key: string, value: any): boolean { 57 | return this.read() 58 | .get(key) 59 | .unset(value) 60 | .value() 61 | } 62 | getById (key: string, id: string) { 63 | return ( 64 | this.read() 65 | .get(key) 66 | // @ts-ignore 67 | .getById(id) 68 | .value() 69 | ) 70 | } 71 | removeById (key: string, id: string) { 72 | return ( 73 | this.read() 74 | .get(key) 75 | // @ts-ignore 76 | .removeById(id) 77 | .write() 78 | ) 79 | } 80 | } 81 | 82 | export default new DB() 83 | -------------------------------------------------------------------------------- /src/universal/types/electron.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BrowserWindow 3 | } from 'electron' 4 | import { 5 | IWindowList 6 | } from 'apis/app/window/constants' 7 | 8 | declare interface IWindowListItem { 9 | isValid: boolean 10 | multiple: boolean 11 | options: () => IBrowserWindowOptions, 12 | callback: (window: BrowserWindow, windowManager: IWindowManager) => void 13 | } 14 | 15 | declare interface IWindowManager { 16 | create: (name: IWindowList) => BrowserWindow | null 17 | get: (name: IWindowList) => BrowserWindow | null 18 | has: (name: IWindowList) => boolean 19 | // delete: (name: IWindowList) => void 20 | deleteById: (id: number) => void 21 | getAvailableWindow: () => BrowserWindow 22 | } 23 | 24 | // https://stackoverflow.com/questions/35074713/extending-typescript-global-object-in-node-js/44387594#44387594 25 | declare global { 26 | namespace NodeJS { 27 | interface Global { 28 | HVP_GUI_VERSION: string 29 | notificationList: IAppNotification[] 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/universal/types/extra-vue.d.ts: -------------------------------------------------------------------------------- 1 | import VueRouter, { Route } from 'vue-router' 2 | import db from '#/datastore' 3 | import axios from 'axios' 4 | declare module 'vue/types/vue' { 5 | interface Vue { 6 | $router: VueRouter; 7 | $route: Route; 8 | $db: typeof db; 9 | $http: typeof axios; 10 | $builtInPicBed: string[]; 11 | $bus: Vue; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/universal/types/shims-module.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | // third-party 6 | declare module 'fix-path' { 7 | function fixPath(): void 8 | export default fixPath 9 | } 10 | -------------------------------------------------------------------------------- /src/universal/types/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue' 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/universal/types/types.d.ts: -------------------------------------------------------------------------------- 1 | // global 2 | interface IObj { 3 | [propName: string]: any 4 | } 5 | 6 | interface IObjT { 7 | [propName: string]: T 8 | } 9 | 10 | declare interface ErrnoException extends Error { 11 | errno?: number | string; 12 | code?: string; 13 | path?: string; 14 | syscall?: string; 15 | stack?: string; 16 | } 17 | 18 | declare var __static: string 19 | 20 | declare type ILogType = 'success' | 'info' | 'warn' | 'error' 21 | 22 | // Server 23 | type routeHandler = (ctx: IServerCTX) => Promise 24 | 25 | type IHttpResponse = import('http').ServerResponse 26 | 27 | interface IServerCTX { 28 | response: IHttpResponse 29 | [propName: string]: any 30 | } 31 | 32 | interface IServerConfig { 33 | port: number | string 34 | host: string 35 | enable: boolean 36 | } 37 | 38 | // Image && PicBed 39 | interface ImgInfo { 40 | buffer?: Buffer 41 | base64Image?: string 42 | fileName?: string 43 | width?: number 44 | height?: number 45 | extname?: string 46 | imgUrl?: string 47 | id?: string 48 | [propName: string]: any 49 | } 50 | 51 | interface IPicBedType { 52 | type: string 53 | name: string 54 | visible: boolean 55 | } 56 | 57 | // Config Settings 58 | interface IShortKeyConfig { 59 | enable: boolean 60 | key: string // 按键 61 | name: string 62 | label: string 63 | from?: string 64 | } 65 | 66 | interface IShortKeyConfigs { 67 | [propName: string]: IShortKeyConfig 68 | } 69 | 70 | interface IOldShortKeyConfigs { 71 | upload: string 72 | } 73 | 74 | interface IKeyCommandType { 75 | key: string, 76 | command: string 77 | } 78 | 79 | // Main process 80 | interface IBrowserWindowOptions { 81 | height: number, 82 | width: number, 83 | show: boolean, 84 | fullscreenable: boolean, 85 | resizable: boolean, 86 | webPreferences: { 87 | nodeIntegration: boolean, 88 | nodeIntegrationInWorker: boolean, 89 | backgroundThrottling: boolean 90 | webSecurity?: boolean 91 | }, 92 | vibrancy?: string | any, 93 | frame?: boolean 94 | center?: boolean 95 | title?: string 96 | titleBarStyle?: string | any 97 | backgroundColor?: string 98 | autoHideMenuBar?: boolean 99 | transparent?: boolean 100 | icon?: string 101 | skipTaskbar?: boolean 102 | alwaysOnTop?: boolean 103 | } 104 | 105 | interface IFileWithPath { 106 | path: string 107 | name?: string 108 | } 109 | 110 | interface IBounds { 111 | x: number 112 | y: number 113 | } 114 | 115 | interface IPluginMenuConfig { 116 | name: string 117 | fullName?: string 118 | config: any[] 119 | } 120 | 121 | interface INPMSearchResult { 122 | data: { 123 | objects: INPMSearchResultObject[] 124 | } 125 | } 126 | 127 | interface INPMSearchResultObject { 128 | package: { 129 | name: string 130 | scope: string 131 | version: string 132 | description: string 133 | keywords: string[] 134 | author: { 135 | name: string 136 | } 137 | links: { 138 | npm: string 139 | homepage: string 140 | } 141 | } 142 | } 143 | 144 | type IDispose = () => void 145 | 146 | // GuiApi 147 | interface IGuiApi { 148 | showInputBox: (options: IShowInputBoxOption) => Promise 149 | showFileExplorer: (options: IShowFileExplorerOption) => Promise 150 | upload: (input: IUploadOption) => Promise 151 | showNotification: (options?: IShowNotificationOption) => void 152 | showMessageBox: (options?: IShowMessageBoxOption) => Promise 153 | } 154 | interface IShowInputBoxOption { 155 | value?: string 156 | title: string 157 | placeholder: string 158 | } 159 | 160 | type IShowFileExplorerOption = IObj 161 | 162 | type IUploadOption = string[] 163 | 164 | interface IShowNotificationOption { 165 | title: string 166 | body: string 167 | } 168 | 169 | interface IShowMessageBoxOption { 170 | title: string 171 | message: string 172 | type: string 173 | buttons: string[] 174 | } 175 | 176 | interface IShowMessageBoxResult { 177 | result: number 178 | checkboxChecked: boolean 179 | } 180 | 181 | // PicBeds 182 | interface IAliYunConfig { 183 | accessKeyId: string 184 | accessKeySecret: string, 185 | bucket: string, 186 | area: string, 187 | path: string, 188 | customUrl: string 189 | options: string 190 | } 191 | 192 | interface IGitHubConfig { 193 | repo: string, 194 | token: string, 195 | path: string, 196 | customUrl: string, 197 | branch: string 198 | } 199 | 200 | interface IImgurConfig { 201 | clientId: string, 202 | proxy: string 203 | } 204 | 205 | interface IQiniuConfig { 206 | accessKey: string, 207 | secretKey: string, 208 | bucket: string, 209 | url: string, 210 | area: string, 211 | options: string, 212 | path: string 213 | } 214 | 215 | interface ISMMSConfig { 216 | token: string 217 | } 218 | 219 | interface ITcYunConfig { 220 | secretId: string, 221 | secretKey: string, 222 | bucket: string, 223 | appId: string, 224 | area: string, 225 | path: string, 226 | customUrl: string, 227 | version: 'v4' | 'v5' 228 | } 229 | 230 | interface IUpYunConfig { 231 | bucket: string, 232 | operator: string, 233 | password: string, 234 | options: string, 235 | path: string 236 | } 237 | 238 | type ILoggerType = string | Error | boolean | number | undefined 239 | 240 | interface IAppNotification { 241 | title: string 242 | body: string 243 | icon?: string 244 | } 245 | -------------------------------------------------------------------------------- /src/universal/types/view.d.ts: -------------------------------------------------------------------------------- 1 | interface ISettingForm { 2 | updateHelper: boolean 3 | showPicBedList: string[] 4 | autoStart: boolean 5 | rename: boolean 6 | autoRename: boolean 7 | uploadNotification: boolean 8 | miniWindowOntop: boolean 9 | logLevel: string[] 10 | autoCopyUrl: boolean 11 | checkBetaUpdate: boolean 12 | } 13 | 14 | interface IShortKeyMap { 15 | [propName: string]: string 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "experimentalDecorators": true, 12 | "allowSyntheticDefaultImports": true, 13 | "sourceMap": true, 14 | "baseUrl": ".", 15 | "types": [ 16 | "webpack-env" 17 | ], 18 | "typeRoots": [ 19 | "./src/universal/types/*", 20 | ], 21 | "paths": { 22 | "@/*": [ 23 | "src/renderer/*" 24 | ], 25 | "~/*": [ 26 | "src/*" 27 | ], 28 | "root/*": [ 29 | "./*" 30 | ], 31 | "#/*": [ 32 | "src/universal/*" 33 | ], 34 | "apis/*": [ 35 | "src/main/apis/*" 36 | ], 37 | "@core/*": [ 38 | "src/main/apis/core/*" 39 | ] 40 | }, 41 | "lib": [ 42 | "esnext", 43 | "dom", 44 | "dom.iterable", 45 | "scripthost" 46 | ] 47 | }, 48 | "include": [ 49 | "src/**/*.ts", 50 | "src/**/*.tsx", 51 | "src/**/*.vue", 52 | "tests/**/*.ts", 53 | "tests/**/*.tsx" 54 | ], 55 | "exclude": [ 56 | "node_modules" 57 | ] 58 | } -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | function resolve (dir) { 4 | return path.join(__dirname, dir) 5 | } 6 | 7 | module.exports = { 8 | chainWebpack: config => { 9 | config.resolve.alias 10 | .set('@', resolve('src/renderer')) 11 | .set('~', resolve('src')) 12 | .set('root', resolve('./')) 13 | .set('#', resolve('src/universal')) 14 | }, 15 | pluginOptions: { 16 | electronBuilder: { 17 | nodeIntegration: true, 18 | customFileProtocol: 'hvp://./', 19 | externals: ['hvp'], 20 | chainWebpackMainProcess: config => { 21 | config.resolve.alias 22 | .set('@', resolve('src/renderer')) 23 | .set('~', resolve('src')) 24 | .set('root', resolve('./')) 25 | .set('#', resolve('src/universal')) 26 | .set('apis', resolve('src/main/apis')) 27 | }, 28 | builderOptions: { 29 | appId: 'com.Lingyan000.hikerviewplayer', 30 | productName: '海阔视界播放器', 31 | publish: [ 32 | { 33 | provider: 'github', 34 | owner: 'Lingyan000', 35 | repo: 'hikerview-player', 36 | releaseType: 'draft' 37 | } 38 | ], 39 | dmg: { 40 | contents: [ 41 | { 42 | x: 410, 43 | y: 150, 44 | type: 'link', 45 | path: '/Applications' 46 | }, 47 | { 48 | x: 130, 49 | y: 150, 50 | type: 'file' 51 | } 52 | ] 53 | }, 54 | copyright: 'Copyright © 2020', 55 | win: { 56 | icon: 'build/icons/icon.ico', 57 | target: [ 58 | { 59 | target: 'nsis', // 利用nsis制作安装程序 60 | arch: [ 61 | 'x64', // 64位 62 | 'ia32' // 32位 63 | ] 64 | } 65 | ] 66 | }, 67 | mac: { 68 | icon: 'build/icons/icon.icns', 69 | extendInfo: { 70 | LSUIElement: 0 71 | } 72 | }, 73 | nsis: { 74 | oneClick: false, // 是否一键安装 75 | allowToChangeInstallationDirectory: true, // 允许修改安装目录 76 | shortcutName: '海阔视界播放器' // 图标名称 77 | }, 78 | linux: { 79 | icon: 'build/icons/' 80 | }, 81 | snap: { 82 | publish: ['github'] 83 | } 84 | } 85 | } 86 | } 87 | } 88 | --------------------------------------------------------------------------------