├── .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 |

3 |
4 | # Hikerview Player
5 |
6 | > 海阔视界网页投屏播放器
7 |
8 | [](https://github.com/Lingyan000/hikerview-player/actions?query=workflow%3ABuild)
9 | [](https://ci.appveyor.com/project/Lingyan000/hikerview-player)
10 | [](https://github.com/Lingyan000/hikerview-player/blob/master/LICENSE)
11 | [](https://github.com/Lingyan000/hikerview-player/releases)
12 | [](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 | 
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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
6 |
7 |
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 |
2 |
11 |
12 |
13 |
14 |
15 | {{ title }}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
87 |
88 |
115 |
--------------------------------------------------------------------------------
/src/renderer/views/Home/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
海阔视界的播放器页面点击网页投屏按钮即可看到IP地址,请确保手机和电脑在同一个局域网
14 |
23 | :52020
24 |
25 |
26 |
27 | 确认
28 |
29 |
30 |
31 |
32 |
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 |
--------------------------------------------------------------------------------