├── .babelrc
├── .electron-vue
├── build.js
├── dev-client.js
├── dev-runner.js
├── webpack.main.config.js
├── webpack.renderer.config.js
└── webpack.web.config.js
├── .gitignore
├── .prettierrc
├── .travis.yml
├── CHANGELOG.md
├── README.md
├── appveyor.yml
├── build
└── icons
│ ├── 256x256.png
│ ├── icon.icns
│ ├── icon.ico
│ └── icon.png
├── dist
├── electron
│ └── .gitkeep
└── web
│ └── .gitkeep
├── package-lock.json
├── package.json
├── src
├── index.ejs
├── main
│ ├── index.dev.js
│ └── index.js
└── renderer
│ ├── App.vue
│ ├── api
│ ├── electron.js
│ ├── job.js
│ ├── message.js
│ ├── notification.js
│ ├── profile.js
│ ├── session.js
│ ├── thumnail.js
│ ├── tool.js
│ ├── upyunClient.js
│ ├── upyunFtp.js
│ └── user.js
│ ├── assets
│ ├── iconfont.js
│ └── icons
│ │ ├── css.svg
│ │ ├── file.svg
│ │ ├── folder.svg
│ │ ├── html.svg
│ │ ├── image.svg
│ │ ├── javascript.svg
│ │ ├── json.svg
│ │ ├── markdown.svg
│ │ ├── movie.svg
│ │ ├── music.svg
│ │ └── zip.svg
│ ├── components
│ ├── ConfirmModal.vue
│ ├── Icon.vue
│ ├── LocalImage.vue
│ ├── ProgressBar.vue
│ ├── ResIcon.vue
│ └── Spinner.vue
│ ├── fonts
│ └── SourceHanSansCN-Light.ttf
│ ├── imgs
│ └── updrive.svg
│ ├── main.js
│ ├── router
│ └── index.js
│ ├── store
│ ├── actions.js
│ ├── getters.js
│ ├── index.js
│ ├── modules
│ │ ├── auth.js
│ │ ├── index.js
│ │ ├── list.js
│ │ ├── modal.js
│ │ ├── profile.js
│ │ └── task.js
│ └── mutation-types.js
│ ├── styles
│ ├── bulma.scss
│ ├── common.scss
│ ├── custom.scss
│ ├── icons.scss
│ ├── index.scss
│ ├── layout.scss
│ ├── listView.scss
│ ├── modal.scss
│ ├── scroll.scss
│ └── task.scss
│ └── views
│ ├── download
│ └── Download.vue
│ ├── layout
│ ├── LayoutBody.vue
│ ├── LayoutMenu.vue
│ ├── LayoutNav.vue
│ └── Main.vue
│ ├── list
│ └── List.vue
│ ├── login
│ └── Login.vue
│ ├── modal
│ ├── CreateFolder.vue
│ ├── DomainSetting.vue
│ ├── FileProgress.vue
│ ├── FormatUrl.vue
│ ├── RenameFile.vue
│ └── UploadHandle.vue
│ └── upload
│ └── Upload.vue
├── static
├── .gitkeep
├── screenshot1.png
├── screenshot2.png
└── screenshot3.png
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "comments": false,
3 | "env": {
4 | "main": {
5 | "presets": [
6 | ["env", {
7 | "targets": { "node": 7 }
8 | }],
9 | "stage-0"
10 | ]
11 | },
12 | "renderer": {
13 | "presets": [
14 | ["env", {
15 | "modules": false
16 | }],
17 | "stage-0"
18 | ]
19 | },
20 | "web": {
21 | "presets": [
22 | ["env", {
23 | "modules": false
24 | }],
25 | "stage-0"
26 | ]
27 | }
28 | },
29 | "plugins": ["transform-runtime"]
30 | }
31 |
--------------------------------------------------------------------------------
/.electron-vue/build.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | process.env.NODE_ENV = 'production'
4 |
5 | const { say } = require('cfonts')
6 | const chalk = require('chalk')
7 | const del = require('del')
8 | const { spawn } = require('child_process')
9 | const webpack = require('webpack')
10 | const Multispinner = require('multispinner')
11 |
12 |
13 | const mainConfig = require('./webpack.main.config')
14 | const rendererConfig = require('./webpack.renderer.config')
15 | const webConfig = require('./webpack.web.config')
16 |
17 | const doneLog = chalk.bgGreen.white(' DONE ') + ' '
18 | const errorLog = chalk.bgRed.white(' ERROR ') + ' '
19 | const okayLog = chalk.bgBlue.white(' OKAY ') + ' '
20 | const isCI = process.env.CI || false
21 |
22 | if (process.env.BUILD_TARGET === 'clean') clean()
23 | else if (process.env.BUILD_TARGET === 'web') web()
24 | else build()
25 |
26 | function clean () {
27 | del.sync(['build/*', '!build/icons', '!build/icons/icon.*'])
28 | console.log(`\n${doneLog}\n`)
29 | process.exit()
30 | }
31 |
32 | function build () {
33 | greeting()
34 |
35 | del.sync(['dist/electron/*', '!.gitkeep'])
36 |
37 | const tasks = ['main', 'renderer']
38 | const m = new Multispinner(tasks, {
39 | preText: 'building',
40 | postText: 'process'
41 | })
42 |
43 | let results = ''
44 |
45 | m.on('success', () => {
46 | process.stdout.write('\x1B[2J\x1B[0f')
47 | console.log(`\n\n${results}`)
48 | console.log(`${okayLog}take it away ${chalk.yellow('`electron-builder`')}\n`)
49 | process.exit()
50 | })
51 |
52 | pack(mainConfig).then(result => {
53 | results += result + '\n\n'
54 | m.success('main')
55 | }).catch(err => {
56 | m.error('main')
57 | console.log(`\n ${errorLog}failed to build main process`)
58 | console.error(`\n${err}\n`)
59 | process.exit(1)
60 | })
61 |
62 | pack(rendererConfig).then(result => {
63 | results += result + '\n\n'
64 | m.success('renderer')
65 | }).catch(err => {
66 | m.error('renderer')
67 | console.log(`\n ${errorLog}failed to build renderer process`)
68 | console.error(`\n${err}\n`)
69 | process.exit(1)
70 | })
71 | }
72 |
73 | function pack (config) {
74 | return new Promise((resolve, reject) => {
75 | webpack(config, (err, stats) => {
76 | if (err) reject(err.stack || err)
77 | else if (stats.hasErrors()) {
78 | let err = ''
79 |
80 | stats.toString({
81 | chunks: false,
82 | colors: true
83 | })
84 | .split(/\r?\n/)
85 | .forEach(line => {
86 | err += ` ${line}\n`
87 | })
88 |
89 | reject(err)
90 | } else {
91 | resolve(stats.toString({
92 | chunks: false,
93 | colors: true
94 | }))
95 | }
96 | })
97 | })
98 | }
99 |
100 | function web () {
101 | del.sync(['dist/web/*', '!.gitkeep'])
102 | webpack(webConfig, (err, stats) => {
103 | if (err || stats.hasErrors()) console.log(err)
104 |
105 | console.log(stats.toString({
106 | chunks: false,
107 | colors: true
108 | }))
109 |
110 | process.exit()
111 | })
112 | }
113 |
114 | function greeting () {
115 | const cols = process.stdout.columns
116 | let text = ''
117 |
118 | if (cols > 85) text = 'lets-build'
119 | else if (cols > 60) text = 'lets-|build'
120 | else text = false
121 |
122 | if (text && !isCI) {
123 | say(text, {
124 | colors: ['yellow'],
125 | font: 'simple3d',
126 | space: false
127 | })
128 | } else console.log(chalk.yellow.bold('\n lets-build'))
129 | console.log()
130 | }
131 |
--------------------------------------------------------------------------------
/.electron-vue/dev-client.js:
--------------------------------------------------------------------------------
1 | const hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
2 |
3 | hotClient.subscribe(event => {
4 | /**
5 | * Reload browser when HTMLWebpackPlugin emits a new index.html
6 | *
7 | * Currently disabled until jantimon/html-webpack-plugin#680 is resolved.
8 | * https://github.com/SimulatedGREG/electron-vue/issues/437
9 | * https://github.com/jantimon/html-webpack-plugin/issues/680
10 | */
11 | // if (event.action === 'reload') {
12 | // window.location.reload()
13 | // }
14 |
15 | /**
16 | * Notify `mainWindow` when `main` process is compiling,
17 | * giving notice for an expected reload of the `electron` process
18 | */
19 | if (event.action === 'compiling') {
20 | document.body.innerHTML += `
21 |
34 |
35 |
36 | Compiling Main Process...
37 |
38 | `
39 | }
40 | })
41 |
--------------------------------------------------------------------------------
/.electron-vue/dev-runner.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const chalk = require('chalk')
4 | const electron = require('electron')
5 | const path = require('path')
6 | const { say } = require('cfonts')
7 | const { spawn } = require('child_process')
8 | const webpack = require('webpack')
9 | const WebpackDevServer = require('webpack-dev-server')
10 | const webpackHotMiddleware = require('webpack-hot-middleware')
11 |
12 | const mainConfig = require('./webpack.main.config')
13 | const rendererConfig = require('./webpack.renderer.config')
14 |
15 | let electronProcess = null
16 | let manualRestart = false
17 | let hotMiddleware
18 |
19 | function logStats (proc, data) {
20 | let log = ''
21 |
22 | log += chalk.yellow.bold(`┏ ${proc} Process ${new Array((19 - proc.length) + 1).join('-')}`)
23 | log += '\n\n'
24 |
25 | if (typeof data === 'object') {
26 | data.toString({
27 | colors: true,
28 | chunks: false
29 | }).split(/\r?\n/).forEach(line => {
30 | log += ' ' + line + '\n'
31 | })
32 | } else {
33 | log += ` ${data}\n`
34 | }
35 |
36 | log += '\n' + chalk.yellow.bold(`┗ ${new Array(28 + 1).join('-')}`) + '\n'
37 |
38 | console.log(log)
39 | }
40 |
41 | function startRenderer () {
42 | return new Promise((resolve, reject) => {
43 | rendererConfig.entry.renderer = [path.join(__dirname, 'dev-client')].concat(rendererConfig.entry.renderer)
44 |
45 | const compiler = webpack(rendererConfig)
46 | hotMiddleware = webpackHotMiddleware(compiler, {
47 | log: false,
48 | heartbeat: 2500
49 | })
50 |
51 | compiler.plugin('compilation', compilation => {
52 | compilation.plugin('html-webpack-plugin-after-emit', (data, cb) => {
53 | hotMiddleware.publish({ action: 'reload' })
54 | cb()
55 | })
56 | })
57 |
58 | compiler.plugin('done', stats => {
59 | logStats('Renderer', stats)
60 | })
61 |
62 | const server = new WebpackDevServer(
63 | compiler,
64 | {
65 | contentBase: path.join(__dirname, '../'),
66 | quiet: true,
67 | before (app, ctx) {
68 | app.use(hotMiddleware)
69 | ctx.middleware.waitUntilValid(() => {
70 | resolve()
71 | })
72 | }
73 | }
74 | )
75 |
76 | server.listen(9080)
77 | })
78 | }
79 |
80 | function startMain () {
81 | return new Promise((resolve, reject) => {
82 | mainConfig.entry.main = [path.join(__dirname, '../src/main/index.dev.js')].concat(mainConfig.entry.main)
83 |
84 | const compiler = webpack(mainConfig)
85 |
86 | compiler.plugin('watch-run', (compilation, done) => {
87 | logStats('Main', chalk.white.bold('compiling...'))
88 | hotMiddleware.publish({ action: 'compiling' })
89 | done()
90 | })
91 |
92 | compiler.watch({}, (err, stats) => {
93 | if (err) {
94 | console.log(err)
95 | return
96 | }
97 |
98 | logStats('Main', stats)
99 |
100 | if (electronProcess && electronProcess.kill) {
101 | manualRestart = true
102 | process.kill(electronProcess.pid)
103 | electronProcess = null
104 | startElectron()
105 |
106 | setTimeout(() => {
107 | manualRestart = false
108 | }, 5000)
109 | }
110 |
111 | resolve()
112 | })
113 | })
114 | }
115 |
116 | function startElectron () {
117 | electronProcess = spawn(electron, ['--inspect=5858', path.join(__dirname, '../dist/electron/main.js')])
118 |
119 | electronProcess.stdout.on('data', data => {
120 | electronLog(data, 'blue')
121 | })
122 | electronProcess.stderr.on('data', data => {
123 | electronLog(data, 'red')
124 | })
125 |
126 | electronProcess.on('close', () => {
127 | if (!manualRestart) process.exit()
128 | })
129 | }
130 |
131 | function electronLog (data, color) {
132 | let log = ''
133 | data = data.toString().split(/\r?\n/)
134 | data.forEach(line => {
135 | log += ` ${line}\n`
136 | })
137 | if (/[0-9A-z]+/.test(log)) {
138 | console.log(
139 | chalk[color].bold('┏ Electron -------------------') +
140 | '\n\n' +
141 | log +
142 | chalk[color].bold('┗ ----------------------------') +
143 | '\n'
144 | )
145 | }
146 | }
147 |
148 | function greeting () {
149 | const cols = process.stdout.columns
150 | let text = ''
151 |
152 | if (cols > 104) text = 'electron-vue'
153 | else if (cols > 76) text = 'electron-|vue'
154 | else text = false
155 |
156 | if (text) {
157 | say(text, {
158 | colors: ['yellow'],
159 | font: 'simple3d',
160 | space: false
161 | })
162 | } else console.log(chalk.yellow.bold('\n electron-vue'))
163 | console.log(chalk.blue(' getting ready...') + '\n')
164 | }
165 |
166 | function init () {
167 | greeting()
168 |
169 | Promise.all([startRenderer(), startMain()])
170 | .then(() => {
171 | startElectron()
172 | })
173 | .catch(err => {
174 | console.error(err)
175 | })
176 | }
177 |
178 | init()
179 |
--------------------------------------------------------------------------------
/.electron-vue/webpack.main.config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | process.env.BABEL_ENV = 'main'
4 |
5 | const path = require('path')
6 | const { dependencies } = require('../package.json')
7 | const webpack = require('webpack')
8 |
9 | const BabiliWebpackPlugin = require('babili-webpack-plugin')
10 |
11 | let mainConfig = {
12 | entry: {
13 | main: path.join(__dirname, '../src/main/index.js')
14 | },
15 | externals: [
16 | ...Object.keys(dependencies || {})
17 | ],
18 | module: {
19 | rules: [
20 | {
21 | test: /\.js$/,
22 | use: 'babel-loader',
23 | exclude: /node_modules/
24 | },
25 | {
26 | test: /\.node$/,
27 | use: 'node-loader'
28 | }
29 | ]
30 | },
31 | node: {
32 | __dirname: process.env.NODE_ENV !== 'production',
33 | __filename: process.env.NODE_ENV !== 'production'
34 | },
35 | output: {
36 | filename: '[name].js',
37 | libraryTarget: 'commonjs2',
38 | path: path.join(__dirname, '../dist/electron')
39 | },
40 | plugins: [
41 | new webpack.NoEmitOnErrorsPlugin()
42 | ],
43 | resolve: {
44 | extensions: ['.js', '.json', '.node']
45 | },
46 | target: 'electron-main'
47 | }
48 |
49 | /**
50 | * Adjust mainConfig for development settings
51 | */
52 | if (process.env.NODE_ENV !== 'production') {
53 | mainConfig.plugins.push(
54 | new webpack.DefinePlugin({
55 | '__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"`
56 | })
57 | )
58 | }
59 |
60 | /**
61 | * Adjust mainConfig for production settings
62 | */
63 | if (process.env.NODE_ENV === 'production') {
64 | mainConfig.plugins.push(
65 | new BabiliWebpackPlugin(),
66 | new webpack.DefinePlugin({
67 | 'process.env.NODE_ENV': '"production"'
68 | })
69 | )
70 | }
71 |
72 | module.exports = mainConfig
73 |
--------------------------------------------------------------------------------
/.electron-vue/webpack.renderer.config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | process.env.BABEL_ENV = 'renderer'
4 |
5 | const path = require('path')
6 | const { dependencies } = require('../package.json')
7 | const webpack = require('webpack')
8 |
9 | const BabiliWebpackPlugin = require('babili-webpack-plugin')
10 | const CopyWebpackPlugin = require('copy-webpack-plugin')
11 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
12 | const HtmlWebpackPlugin = require('html-webpack-plugin')
13 |
14 | /**
15 | * List of node_modules to include in webpack bundle
16 | *
17 | * Required for specific packages like Vue UI libraries
18 | * that provide pure *.vue files that need compiling
19 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/webpack-configurations.html#white-listing-externals
20 | */
21 | let whiteListedModules = ['vue']
22 |
23 | let rendererConfig = {
24 | devtool: '#cheap-module-eval-source-map',
25 | entry: {
26 | renderer: path.join(__dirname, '../src/renderer/main.js')
27 | },
28 | externals: [
29 | ...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d))
30 | ],
31 | module: {
32 | rules: [
33 | {
34 | test: /\.css$/,
35 | use: ExtractTextPlugin.extract({
36 | fallback: 'style-loader',
37 | use: 'css-loader'
38 | })
39 | },
40 | {
41 | test: /\.html$/,
42 | use: 'vue-html-loader'
43 | },
44 | {
45 | test: /\.js$/,
46 | use: 'babel-loader',
47 | exclude: /node_modules/
48 | },
49 | {
50 | test: /\.node$/,
51 | use: 'node-loader'
52 | },
53 | {
54 | test: /\.vue$/,
55 | use: {
56 | loader: 'vue-loader',
57 | options: {
58 | extractCSS: process.env.NODE_ENV === 'production',
59 | loaders: {
60 | sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1',
61 | scss: 'vue-style-loader!css-loader!sass-loader'
62 | }
63 | }
64 | }
65 | },
66 | {
67 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
68 | use: {
69 | loader: 'url-loader',
70 | query: {
71 | limit: 10000,
72 | name: 'imgs/[name]--[folder].[ext]'
73 | }
74 | }
75 | },
76 | {
77 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
78 | loader: 'url-loader',
79 | options: {
80 | limit: 10000,
81 | name: 'media/[name]--[folder].[ext]'
82 | }
83 | },
84 | {
85 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
86 | use: {
87 | loader: 'url-loader',
88 | query: {
89 | limit: 10000,
90 | name: 'fonts/[name]--[folder].[ext]'
91 | }
92 | }
93 | }
94 | ]
95 | },
96 | node: {
97 | __dirname: process.env.NODE_ENV !== 'production',
98 | __filename: process.env.NODE_ENV !== 'production'
99 | },
100 | plugins: [
101 | new ExtractTextPlugin('styles.css'),
102 | new HtmlWebpackPlugin({
103 | filename: 'index.html',
104 | template: path.resolve(__dirname, '../src/index.ejs'),
105 | minify: {
106 | collapseWhitespace: true,
107 | removeAttributeQuotes: true,
108 | removeComments: true
109 | },
110 | nodeModules: process.env.NODE_ENV !== 'production'
111 | ? path.resolve(__dirname, '../node_modules')
112 | : false
113 | }),
114 | new webpack.HotModuleReplacementPlugin(),
115 | new webpack.NoEmitOnErrorsPlugin()
116 | ],
117 | output: {
118 | filename: '[name].js',
119 | libraryTarget: 'commonjs2',
120 | path: path.join(__dirname, '../dist/electron')
121 | },
122 | resolve: {
123 | alias: {
124 | '@': path.join(__dirname, '../src/renderer'),
125 | 'vue$': 'vue/dist/vue.esm.js'
126 | },
127 | extensions: ['.js', '.vue', '.json', '.css', '.node']
128 | },
129 | target: 'electron-renderer'
130 | }
131 |
132 | /**
133 | * Adjust rendererConfig for development settings
134 | */
135 | if (process.env.NODE_ENV !== 'production') {
136 | rendererConfig.plugins.push(
137 | new webpack.DefinePlugin({
138 | '__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"`
139 | })
140 | )
141 | }
142 |
143 | /**
144 | * Adjust rendererConfig for production settings
145 | */
146 | if (process.env.NODE_ENV === 'production') {
147 | rendererConfig.devtool = ''
148 |
149 | rendererConfig.plugins.push(
150 | new BabiliWebpackPlugin(),
151 | new CopyWebpackPlugin([
152 | {
153 | from: path.join(__dirname, '../static'),
154 | to: path.join(__dirname, '../dist/electron/static'),
155 | ignore: ['.*']
156 | }
157 | ]),
158 | new webpack.DefinePlugin({
159 | 'process.env.NODE_ENV': '"production"'
160 | }),
161 | new webpack.LoaderOptionsPlugin({
162 | minimize: true
163 | })
164 | )
165 | }
166 |
167 | module.exports = rendererConfig
168 |
--------------------------------------------------------------------------------
/.electron-vue/webpack.web.config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | process.env.BABEL_ENV = 'web'
4 |
5 | const path = require('path')
6 | const webpack = require('webpack')
7 |
8 | const BabiliWebpackPlugin = require('babili-webpack-plugin')
9 | const CopyWebpackPlugin = require('copy-webpack-plugin')
10 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
11 | const HtmlWebpackPlugin = require('html-webpack-plugin')
12 |
13 | let webConfig = {
14 | devtool: '#cheap-module-eval-source-map',
15 | entry: {
16 | web: path.join(__dirname, '../src/renderer/main.js')
17 | },
18 | module: {
19 | rules: [
20 | {
21 | test: /\.css$/,
22 | use: ExtractTextPlugin.extract({
23 | fallback: 'style-loader',
24 | use: 'css-loader'
25 | })
26 | },
27 | {
28 | test: /\.html$/,
29 | use: 'vue-html-loader'
30 | },
31 | {
32 | test: /\.js$/,
33 | use: 'babel-loader',
34 | include: [ path.resolve(__dirname, '../src/renderer') ],
35 | exclude: /node_modules/
36 | },
37 | {
38 | test: /\.vue$/,
39 | use: {
40 | loader: 'vue-loader',
41 | options: {
42 | extractCSS: true,
43 | loaders: {
44 | sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1',
45 | scss: 'vue-style-loader!css-loader!sass-loader'
46 | }
47 | }
48 | }
49 | },
50 | {
51 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
52 | use: {
53 | loader: 'url-loader',
54 | query: {
55 | limit: 10000,
56 | name: 'imgs/[name].[ext]'
57 | }
58 | }
59 | },
60 | {
61 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
62 | use: {
63 | loader: 'url-loader',
64 | query: {
65 | limit: 10000,
66 | name: 'fonts/[name].[ext]'
67 | }
68 | }
69 | }
70 | ]
71 | },
72 | plugins: [
73 | new ExtractTextPlugin('styles.css'),
74 | new HtmlWebpackPlugin({
75 | filename: 'index.html',
76 | template: path.resolve(__dirname, '../src/index.ejs'),
77 | minify: {
78 | collapseWhitespace: true,
79 | removeAttributeQuotes: true,
80 | removeComments: true
81 | },
82 | nodeModules: false
83 | }),
84 | new webpack.DefinePlugin({
85 | 'process.env.IS_WEB': 'true'
86 | }),
87 | new webpack.HotModuleReplacementPlugin(),
88 | new webpack.NoEmitOnErrorsPlugin()
89 | ],
90 | output: {
91 | filename: '[name].js',
92 | path: path.join(__dirname, '../dist/web')
93 | },
94 | resolve: {
95 | alias: {
96 | '@': path.join(__dirname, '../src/renderer'),
97 | 'vue$': 'vue/dist/vue.esm.js'
98 | },
99 | extensions: ['.js', '.vue', '.json', '.css']
100 | },
101 | target: 'web'
102 | }
103 |
104 | /**
105 | * Adjust webConfig for production settings
106 | */
107 | if (process.env.NODE_ENV === 'production') {
108 | webConfig.devtool = ''
109 |
110 | webConfig.plugins.push(
111 | new BabiliWebpackPlugin(),
112 | new CopyWebpackPlugin([
113 | {
114 | from: path.join(__dirname, '../static'),
115 | to: path.join(__dirname, '../dist/web/static'),
116 | ignore: ['.*']
117 | }
118 | ]),
119 | new webpack.DefinePlugin({
120 | 'process.env.NODE_ENV': '"production"'
121 | }),
122 | new webpack.LoaderOptionsPlugin({
123 | minimize: true
124 | })
125 | )
126 | }
127 |
128 | module.exports = webConfig
129 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | dist/electron/*
3 | dist/web/*
4 | build/*
5 | !build/icons
6 | node_modules/
7 | npm-debug.log
8 | npm-debug.log.*
9 | thumbs.db
10 | !.gitkeep
11 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "semi": false,
4 | "singleQuote": true,
5 | "trailingComma": "all"
6 | }
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | osx_image: xcode8.3
2 | sudo: required
3 | dist: trusty
4 | language: c
5 | matrix:
6 | include:
7 | - os: osx
8 | - os: linux
9 | env: CC=clang CXX=clang++ npm_config_clang=1
10 | compiler: clang
11 | cache:
12 | directories:
13 | - node_modules
14 | - "$HOME/.electron"
15 | - "$HOME/.cache"
16 | addons:
17 | apt:
18 | packages:
19 | - libgnome-keyring-dev
20 | - icnsutils
21 | before_install:
22 | - |
23 | if [ "$TRAVIS_OS_NAME" == "osx" ]; then
24 | mkdir -p /tmp/git-lfs && curl -L https://github.com/github/git-lfs/releases/download/v2.3.1/git-lfs-$([ "$TRAVIS_OS_NAME" == "linux" ] && echo "linux" || echo "darwin")-amd64-2.3.1.tar.gz | tar -xz -C /tmp/git-lfs --strip-components 1
25 | export PATH="/tmp/git-lfs:$PATH"
26 | fi
27 | before_script:
28 | - git lfs pull
29 | install:
30 | - nvm install 8
31 | - curl -o- -L https://yarnpkg.com/install.sh | bash
32 | - source ~/.bashrc
33 | - npm install -g xvfb-maybe
34 | - yarn
35 | script:
36 | - yarn run build
37 | branches:
38 | only:
39 | - master
40 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 更新日志
2 |
3 | ## 0.10.0
4 |
5 | ### 新增
6 | - 增加复制粘贴功能
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # updrive
2 |
3 | > 又拍云文件管理桌面客户端
4 | #### ChangeLog
5 |
6 | #### v0.38.0(2018-05-23)
7 | - 修复无法设置加速域名问题
8 |
9 | #### v0.37.0(2018-05-20)
10 | - 上传下载列表,显示本地图片
11 |
12 | ##### v0.36.0(2018-05-05)
13 | - 修复退出应用快捷键错误
14 | - 重构任务列表模块
15 | - 修改应用默认的宽高
16 |
17 | ##### v0.34.0(2018-04-22)
18 | - 获取链接可编辑。
19 | - 增加获取任务列表里面上传完成后的文件获取链接的功能
20 | - 上传下载完成后,增加桌面通知功能
21 | - 任务列表的文件可以双击和单击文件名用操作系统默认程序打开
22 |
23 | ##### v0.33.0
24 | - 增加自定义加速域名功能
25 |
26 | ##### v0.31.0
27 | - 绕过防盗链限制预览图片
28 |
29 | ##### v0.30.0
30 | - 修复不同账号切换存在的一些 BUG
31 | - 手动更新功能
32 | - 增加用户名服务名展示以及设置功能
33 | - 增加云存储服务使用量展示
34 |
35 | #### Download
36 | [下载地址](https://github.com/aniiantt/updrive/releases)
37 |
38 | #### Usage
39 | [云存储服务及操作员账号创建](https://console.upyun.com/services/create/file/)
40 |
41 | #### Screenshot
42 | 
43 | 
44 | 
45 |
46 | #### Build
47 |
48 | ``` bash
49 | # 安装依赖
50 | yarn
51 |
52 | # 启动
53 | yarn dev
54 |
55 | # 打包
56 | yarn build
57 |
58 | ```
59 |
60 | #### Feature
61 | - 基础的文件上传、下载、删除、重命名、查看功能
62 | - 按名称、日期、类型、大小排序
63 | - 批量删除、新建和上传
64 | - 拖曳操作
65 | - 复制链接
66 | - 查看文件响应头
67 | - 多选删除上传
68 | - 上传下载展示,以及历史记录
69 | - 账号历史
70 | - 右键菜单
71 | - 快捷键操作
72 | - 前进,后退功能
73 | - 版本号显示以及检查更新功能
74 | - 切换用户
75 | - 额外链接
76 | - 绕过防盗链
77 |
78 | #### TODO
79 | - [ ] 上传优化
80 | - [ ] 优化快捷键操作。
81 | - [ ] 拆分任务列表,分为上传列表和下载列表
82 | - [ ] 列表筛选
83 | - [ ] 收藏列表
84 | - [ ] 升级下载进度,自动选择文件夹
85 | - [ ] 自定义缩略图版本查看
86 | - [ ] 优化文件查看体验,双击查看详情,文件编辑
87 | - [ ] 托盘图标
88 | - [ ] 列表卡片查看模式,以及瀑布流加载
89 | - [ ] 优化上传下载模块,使用 indexDB 重构
90 | - [ ] 截图上传
91 | - [ ] 文件拉取
92 | - [ ] 云处理功能
93 |
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | version: 0.1.{build}
2 |
3 | branches:
4 | only:
5 | - master
6 |
7 | image: Visual Studio 2017
8 | platform:
9 | - x64
10 |
11 | cache:
12 | - node_modules
13 | - '%APPDATA%\npm-cache'
14 | - '%USERPROFILE%\.electron'
15 | - '%USERPROFILE%\AppData\Local\Yarn\cache'
16 |
17 | init:
18 | - git config --global core.autocrlf input
19 |
20 | install:
21 | - ps: Install-Product node 8 x64
22 | - git reset --hard HEAD
23 | - yarn
24 | - node --version
25 |
26 | build_script:
27 | - yarn build
28 |
29 | test: off
30 |
--------------------------------------------------------------------------------
/build/icons/256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ntduan/updrive/8000819a68671e52e1ad802e0b50a686e5a3817e/build/icons/256x256.png
--------------------------------------------------------------------------------
/build/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ntduan/updrive/8000819a68671e52e1ad802e0b50a686e5a3817e/build/icons/icon.icns
--------------------------------------------------------------------------------
/build/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ntduan/updrive/8000819a68671e52e1ad802e0b50a686e5a3817e/build/icons/icon.ico
--------------------------------------------------------------------------------
/build/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ntduan/updrive/8000819a68671e52e1ad802e0b50a686e5a3817e/build/icons/icon.png
--------------------------------------------------------------------------------
/dist/electron/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ntduan/updrive/8000819a68671e52e1ad802e0b50a686e5a3817e/dist/electron/.gitkeep
--------------------------------------------------------------------------------
/dist/web/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ntduan/updrive/8000819a68671e52e1ad802e0b50a686e5a3817e/dist/web/.gitkeep
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "updrive",
3 | "version": "0.38.0",
4 | "author": "nsso ",
5 | "description": "upyun file manager",
6 | "engines": {
7 | "node": ">=8.2.1"
8 | },
9 | "main": "./dist/electron/main.js",
10 | "scripts": {
11 | "build": "node .electron-vue/build.js && electron-builder",
12 | "build:dir": "node .electron-vue/build.js && electron-builder --dir",
13 | "build:clean": "cross-env BUILD_TARGET=clean node .electron-vue/build.js",
14 | "build:web": "cross-env BUILD_TARGET=web node .electron-vue/build.js",
15 | "dev": "node .electron-vue/dev-runner.js",
16 | "pack": "npm run pack:main && npm run pack:renderer",
17 | "pack:main": "cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.main.config.js",
18 | "pack:renderer": "cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.renderer.config.js",
19 | "postinstall": ""
20 | },
21 | "build": {
22 | "productName": "updrive",
23 | "artifactName": "${productName}.${ext}",
24 | "appId": "org.upyun.updrive",
25 | "directories": {
26 | "output": "build"
27 | },
28 | "files": [
29 | "dist/electron/**/*"
30 | ],
31 | "dmg": {
32 | "contents": [
33 | {
34 | "x": 410,
35 | "y": 150,
36 | "type": "link",
37 | "path": "/Applications"
38 | },
39 | {
40 | "x": 130,
41 | "y": 150,
42 | "type": "file"
43 | }
44 | ]
45 | },
46 | "mac": {
47 | "icon": "build/icons/icon.icns"
48 | },
49 | "win": {
50 | "icon": "build/icons/icon.ico",
51 | "target": [
52 | "nsis",
53 | "portable",
54 | "zip"
55 | ]
56 | },
57 | "linux": {
58 | "icon": "build/icons"
59 | },
60 | "nsis": {
61 | "createDesktopShortcut": true
62 | }
63 | },
64 | "dependencies": {
65 | "axios": "^0.17.1",
66 | "balloon-css": "^0.5.0",
67 | "bulma": "^0.7.0",
68 | "ftp": "^0.3.10",
69 | "iview": "^2.7.2",
70 | "localforage": "^1.5.3",
71 | "mime": "^2.0.3",
72 | "moment": "^2.19.2",
73 | "node-sass": "^4.6.1",
74 | "progressbar.js": "^1.0.1",
75 | "ramda": "^0.25.0",
76 | "request": "^2.83.0",
77 | "sass-loader": "^6.0.6",
78 | "semver": "^5.5.0",
79 | "vue": "^2.5.3",
80 | "vue-electron": "^1.0.6",
81 | "vue-router": "^3.0.1",
82 | "vuex": "^3.0.1"
83 | },
84 | "devDependencies": {
85 | "babel-core": "^6.26.0",
86 | "babel-loader": "^7.1.2",
87 | "babel-plugin-transform-runtime": "^6.23.0",
88 | "babel-preset-env": "^1.6.1",
89 | "babel-preset-stage-0": "^6.24.1",
90 | "babel-register": "^6.26.0",
91 | "babili-webpack-plugin": "^0.1.2",
92 | "cfonts": "^1.1.3",
93 | "chalk": "^2.3.0",
94 | "copy-webpack-plugin": "^4.2.0",
95 | "cross-env": "^5.1.1",
96 | "css-loader": "^0.28.7",
97 | "del": "^3.0.0",
98 | "devtron": "^1.4.0",
99 | "electron": "^2.0.0",
100 | "electron-builder": "^19.45.4",
101 | "electron-debug": "^1.4.0",
102 | "electron-devtools-installer": "^2.2.4",
103 | "extract-text-webpack-plugin": "^3.0.2",
104 | "file-loader": "^1.1.5",
105 | "html-webpack-plugin": "^2.30.1",
106 | "json-loader": "^0.5.7",
107 | "multispinner": "^0.2.1",
108 | "style-loader": "^0.19.0",
109 | "url-loader": "^0.6.2",
110 | "vue-html-loader": "^1.2.4",
111 | "vue-loader": "^13.5.0",
112 | "vue-style-loader": "^3.0.3",
113 | "vue-template-compiler": "^2.5.3",
114 | "webpack": "^3.8.1",
115 | "webpack-dev-server": "^2.9.4",
116 | "webpack-hot-middleware": "^2.20.0"
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | updrive
7 | <% if (htmlWebpackPlugin.options.nodeModules) { %>
8 |
9 |
12 | <% } %>
13 |
14 |
15 |
16 |
17 |
18 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/main/index.dev.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is used specifically and only for development. It installs
3 | * `electron-debug` & `vue-devtools`. There shouldn't be any need to
4 | * modify this file, but it can be used to extend your development
5 | * environment.
6 | */
7 |
8 | /* eslint-disable */
9 |
10 | // Set environment for development
11 | process.env.NODE_ENV = 'development'
12 |
13 | // Install `electron-debug` with `devtron`
14 | require('electron-debug')({ showDevTools: true })
15 |
16 | // Install `vue-devtools`
17 | require('electron').app.on('ready', () => {
18 | let installExtension = require('electron-devtools-installer')
19 | installExtension.default(installExtension.VUEJS_DEVTOOLS)
20 | .then(() => {})
21 | .catch(err => {
22 | console.log('Unable to install `vue-devtools`: \n', err)
23 | })
24 | })
25 |
26 | // Require `main` process to boot app
27 | require('./index')
28 |
--------------------------------------------------------------------------------
/src/main/index.js:
--------------------------------------------------------------------------------
1 | import { app, BrowserWindow } from 'electron'
2 |
3 | /**
4 | * Set `__static` path to static files in production
5 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-static-assets.html
6 | */
7 | if (process.env.NODE_ENV !== 'development') {
8 | global.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\')
9 | }
10 |
11 | let mainWindow
12 | const winURL = process.env.NODE_ENV === 'development'
13 | ? `http://localhost:9080`
14 | : `file://${__dirname}/index.html`
15 |
16 | function createWindow () {
17 | /**
18 | * Initial window options
19 | */
20 | mainWindow = new BrowserWindow({
21 | useContentSize: true,
22 | height: 625,
23 | width: 980,
24 | minHeight: 525,
25 | minWidth: 980,
26 | webPreferences: { webSecurity: false},
27 | })
28 |
29 | mainWindow.loadURL(winURL)
30 |
31 | mainWindow.on('closed', () => {
32 | mainWindow = null
33 | })
34 | }
35 |
36 | app.on('ready', createWindow)
37 |
38 | app.on('window-all-closed', () => {
39 | if (process.platform !== 'darwin') {
40 | app.quit()
41 | }
42 | })
43 |
44 | app.on('activate', () => {
45 | if (mainWindow === null) {
46 | createWindow()
47 | }
48 | })
49 |
50 | /**
51 | * Auto Updater
52 | *
53 | * Uncomment the following code below and install `electron-updater` to
54 | * support auto updating. Code Signing with a valid certificate is required.
55 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-electron-builder.html#auto-updating
56 | */
57 |
58 | /*
59 | import { autoUpdater } from 'electron-updater'
60 |
61 | autoUpdater.on('update-downloaded', () => {
62 | autoUpdater.quitAndInstall()
63 | })
64 |
65 | app.on('ready', () => {
66 | if (process.env.NODE_ENV === 'production') autoUpdater.checkForUpdates()
67 | })
68 | */
69 |
--------------------------------------------------------------------------------
/src/renderer/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
28 |
29 |
32 |
--------------------------------------------------------------------------------
/src/renderer/api/electron.js:
--------------------------------------------------------------------------------
1 | import { ipcRenderer, shell, clipboard, remote, webFrame } from 'electron'
2 |
3 | import Router from '@/router'
4 | import Store from '@/store'
5 | import { externalUrls } from '@/api/tool'
6 |
7 | const { app, dialog, Menu, MenuItem, BrowserWindow, getCurrentWindow } = remote
8 |
9 | const currentWin = getCurrentWindow()
10 |
11 | const session = currentWin.webContents.session
12 |
13 | const userAgent = `${process.env.npm_package_build_productName}/${process.env.npm_package_version}`
14 |
15 | // 禁止缩放
16 | webFrame.setVisualZoomLevelLimits(1, 1)
17 |
18 | // img 标签注入授权头
19 | session.webRequest.onBeforeSendHeaders(
20 | {
21 | urls: ['*://v0.api.upyun.com/*'],
22 | },
23 | (details, callback) => {
24 | if (details.resourceType === 'image') {
25 | const authHeaders = Store.getters.upyunClient.getHeaders(details.url)
26 | callback({
27 | requestHeaders: {
28 | ...details.requestHeaders,
29 | ...authHeaders,
30 | },
31 | })
32 | } else {
33 | callback({})
34 | }
35 | },
36 | )
37 |
38 | // 聚焦
39 | export const winShow = currentWin.show
40 |
41 | // 设置菜单
42 | export const setApplicationMenu = () => {
43 | const menu = [
44 | {
45 | label: '文件',
46 | submenu: [
47 | {
48 | label: '切换账号',
49 | click() {
50 | Router.push({ name: 'login' })
51 | Store.dispatch('LOGOUT')
52 | },
53 | },
54 | {
55 | label: '退出',
56 | role: 'quit',
57 | },
58 | ],
59 | },
60 | {
61 | label: ' 编辑',
62 | submenu: [
63 | {
64 | label: '撤销',
65 | role: 'undo',
66 | },
67 | {
68 | label: '恢复',
69 | role: 'redo',
70 | },
71 | {
72 | type: 'separator',
73 | },
74 | {
75 | label: '复制',
76 | role: 'copy',
77 | },
78 | {
79 | label: '粘贴',
80 | role: 'paste',
81 | },
82 | {
83 | label: '剪切',
84 | role: 'cut',
85 | },
86 | ],
87 | },
88 | {
89 | label: '查看',
90 | submenu: [
91 | {
92 | label: '刷新',
93 | role: 'reload',
94 | },
95 | ],
96 | },
97 | {
98 | label: '帮助',
99 | role: 'help',
100 | submenu: [
101 | {
102 | label: '切换开发人员工具',
103 | role: 'toggledevtools',
104 | },
105 | {
106 | label: '报告一个问题',
107 | click() {
108 | shell.openExternal(externalUrls.issues)
109 | },
110 | },
111 | {
112 | type: 'separator',
113 | },
114 | {
115 | label: '关于',
116 | click() {
117 | shell.openExternal(externalUrls.repository)
118 | },
119 | },
120 | ],
121 | },
122 | ]
123 | Menu.setApplicationMenu(Menu.buildFromTemplate(menu))
124 | }
125 |
126 | export const writeText = clipboard.writeText
127 |
128 | // 打开外部链接
129 | export const openExternal = shell.openExternal
130 |
131 | export const windowOpen = (url, frameName, features) => {
132 | let child = new BrowserWindow({ parent: currentWin, modal: true, show: false })
133 | child.loadURL(url)
134 | child.once('ready-to-show', () => {
135 | child.show()
136 | })
137 | }
138 |
139 | // 创建右键菜单
140 | export const createContextmenu = ({ appendItems } = {}) => {
141 | const menu = new Menu()
142 | for (const menuItem of appendItems) {
143 | if (!menuItem.hide) menu.append(new MenuItem(menuItem))
144 | }
145 | return menu
146 | }
147 |
148 | // 显示右键菜单
149 | export const showContextmenu = (items, opts = {}) => {
150 | const menu = createContextmenu(items)
151 | setTimeout(() => menu.popup(currentWin))
152 | }
153 |
154 | // 获取版本号
155 | export const getVersion = app.getVersion
156 |
157 | // 获取产品名称
158 | export const getName = app.getName
159 |
160 | // 监听 Ctrl + A
161 | export const listenSelectAll = callback => ipcRenderer.on('SHORTCUT_SELECT_ALL', callback)
162 |
163 | // 上传文件
164 | export const uploadFileDialog = (option = {}) => {
165 | return new Promise((resolve, reject) => {
166 | dialog.showOpenDialog(
167 | currentWin,
168 | {
169 | title: '选择要上传的文件',
170 | buttonLabel: '上传',
171 | properties: ['openFile', 'multiSelections'],
172 | ...option,
173 | },
174 | resolve,
175 | )
176 | })
177 | }
178 |
179 | // 上传文件夹
180 | export const uploadDirectoryDialog = (option = {}) => {
181 | return new Promise((resolve, reject) => {
182 | dialog.showOpenDialog(
183 | currentWin,
184 | {
185 | title: '选择要上传的文件夹',
186 | buttonLabel: '上传',
187 | properties: ['openDirectory', 'createDirectory', 'multiSelections', 'showHiddenFiles'],
188 | ...option,
189 | },
190 | resolve,
191 | )
192 | })
193 | }
194 |
195 | // 下载
196 | export const downloadFileDialog = (option = {}) => {
197 | return new Promise((resolve, reject) => {
198 | dialog.showOpenDialog(
199 | currentWin,
200 | {
201 | title: '下载到',
202 | buttonLabel: '保存',
203 | properties: ['openDirectory', 'createDirectory', 'showHiddenFiles'],
204 | ...option,
205 | },
206 | folderPaths => {
207 | resolve(folderPaths && folderPaths[0])
208 | },
209 | )
210 | })
211 | }
212 |
213 | export const showItemInFolder = fullPath => {
214 | return shell.showItemInFolder(fullPath)
215 | }
216 |
217 | export const openItem = fullPath => {
218 | return shell.openItem(fullPath)
219 | }
220 |
--------------------------------------------------------------------------------
/src/renderer/api/job.js:
--------------------------------------------------------------------------------
1 | import Request from 'request'
2 | import EventEmitter from 'events'
3 | import Fs from 'fs'
4 | import { basename } from 'path'
5 | import { prepend, groupBy } from 'ramda'
6 | import localforage from 'localforage'
7 | import moment from 'moment'
8 |
9 | import { base64, throttle } from '@/api/tool'
10 |
11 | // 上传和下载应该分开
12 | // 大量文件下性能问题未知
13 |
14 | const Job = {
15 | initStore: {
16 | version: 0.1,
17 | data: [],
18 | },
19 |
20 | status: {
21 | downloading: { name: '下载中...', value: 'downloading' },
22 | uploading: { name: '上传中...', value: 'uploading' },
23 | interrupted: { name: '已暂停', value: 'interrupted' },
24 | completed: { name: '已完成', value: 'completed' },
25 | error: { name: '错误', value: 'error' },
26 | },
27 |
28 | setup(keyPre, onChange) {
29 | this.storeKey = `${keyPre}:job`
30 | this.on('change', onChange)
31 | },
32 |
33 | async createDownloadItem(url, localPath) {
34 | const filename = basename(localPath)
35 | const startTime = moment().unix()
36 | const id = base64(`${filename}:${startTime}`)
37 | const item = {
38 | id: id, // 唯一ID
39 | url: url, // 远程路径
40 | connectType: 'download', // 类型 download upload
41 | localPath: localPath, // 下载本地路径
42 | startTime: startTime, // 下载开始时间
43 | filename: basename(localPath), // 下载的文件名
44 | status: this.status.downloading.value, // 下载状态: "uploading", "downloading", "interrupted", "completed", "error"
45 | errorMessage: '',
46 | transferred: 0, // 已下载大小
47 | total: -1, // 总共大小
48 | endTime: -1, // 下载结束时间
49 | }
50 | // await this.setItem(id, item) 一开始不存储任务
51 | this.emit('change', { ...item })
52 | return item
53 | },
54 |
55 | async createUploadItem(url, localPath) {
56 | const filename = decodeURIComponent(new URL(url).pathname.split('/').reverse()[0])
57 | const startTime = moment().unix()
58 | const total = Fs.statSync(localPath).size
59 | const id = base64(`${filename}:${startTime}`)
60 | const item = {
61 | id: id, // 唯一ID
62 | url: url, // 上传路径
63 | connectType: 'upload', // 类型 download upload
64 | localPath: localPath, // 上传本地文件路径
65 | startTime: startTime, // 上传开始时间
66 | filename: filename, // 上传的文件名
67 | status: this.status.uploading.value, // 传输状态: "downloading", "interrupted", "completed", "error"
68 | errorMessage: '',
69 | transferred: 0, // 已上传大小
70 | total: total, // 总共大小
71 | endTime: -1, // 下载结束时间
72 | }
73 | // await this.setItem(id, item) 一开始不存储任务
74 | this.emit('change', { ...item })
75 | return item
76 | },
77 |
78 | async setItem(id, item) {
79 | const store = await this.getStore()
80 | const existedItemIndex = store.data.findIndex(_item => _item.id === id)
81 | if (~existedItemIndex) {
82 | store.data[existedItemIndex] = { ...item }
83 | } else {
84 | store.data = prepend(item, store.data)
85 | }
86 | return await localforage.setItem(this.storeKey, store)
87 | },
88 |
89 | async createDownloadTask({ url, headers, localPath }) {
90 | // @TODO 并发
91 | return new Promise(async (resolve, reject) => {
92 | const item = await this.createDownloadItem(url, localPath)
93 |
94 | const emitChange = () => {
95 | this.emit('change', { ...item })
96 | }
97 |
98 | let percentage = 0
99 | const calTrans = () => {
100 | const newPercentage = (item.transferred / item.total).toFixed(2)
101 | if (percentage !== newPercentage) {
102 | percentage = newPercentage
103 | emitChange()
104 | }
105 | }
106 |
107 | const throttleChunk = throttle(calTrans, 100)
108 |
109 | const request = Request({ url: url, headers: headers })
110 | .on('response', response => {
111 | item.total = window.parseInt(response.headers['content-length'], 10)
112 | emitChange()
113 | })
114 | .on('data', chunk => {
115 | item.transferred += chunk.length
116 | if (item.transferred === item.total) {
117 | calTrans()
118 | } else {
119 | throttleChunk()
120 | }
121 | })
122 | .on('error', async error => {
123 | item.status = this.status.error.value
124 | item.errorMessage = error && error.message
125 | await this.setItem(item.id, item)
126 | emitChange()
127 | reject(error)
128 | })
129 |
130 | const localStream = Fs.createWriteStream(localPath).once('finish', async () => {
131 | item.status = this.status.completed.value
132 | item.endTime = moment().unix()
133 | request.removeAllListeners()
134 | await this.setItem(item.id, item)
135 | emitChange()
136 | resolve('success')
137 | })
138 |
139 | request.pipe(localStream)
140 | })
141 | },
142 |
143 | async createUploadTask({ url, headers, localPath }) {
144 | // @TODO 并发
145 | return new Promise(async (resolve, reject) => {
146 | const item = await this.createUploadItem(url, localPath)
147 | const emitChange = () => {
148 | this.emit('change', { ...item })
149 | }
150 |
151 | let percentage = 0
152 | const calTrans = () => {
153 | const newPercentage = (item.transferred / item.total).toFixed(2)
154 | if (percentage !== newPercentage) {
155 | percentage = newPercentage
156 | emitChange()
157 | }
158 | }
159 |
160 | const throttleChunk = throttle(calTrans, 100)
161 |
162 | const readStream = Fs.createReadStream(localPath).on('data', chunk => {
163 | item.transferred += chunk.length
164 | if (item.transferred === item.total) {
165 | calTrans()
166 | } else {
167 | throttleChunk()
168 | }
169 | })
170 |
171 | const request = Request({
172 | url: url,
173 | headers: {
174 | ...headers,
175 | 'Content-Length': item.total,
176 | },
177 | method: 'PUT',
178 | })
179 | .on('response', async response => {
180 | console.log(response)
181 | if (response.statusCode === 200) {
182 | item.status = this.status.completed.value
183 | item.endTime = moment().unix()
184 | readStream.removeAllListeners()
185 | await this.setItem(item.id, item)
186 | emitChange()
187 | resolve('success')
188 | } else {
189 | item.status = this.status.error.value
190 | item.errorMessage = `${response.statusCode}`
191 | readStream.removeAllListeners()
192 | await this.setItem(item.id, item)
193 | emitChange()
194 | reject(`${response.statusCode}`)
195 | }
196 | })
197 | .on('error', async error => {
198 | item.status = this.status.error.value
199 | item.errorMessage = error && error.message
200 | readStream.removeAllListeners()
201 | await this.setItem(item.id, item)
202 | emitChange()
203 | reject(error)
204 | })
205 |
206 | readStream.pipe(request)
207 | })
208 | },
209 |
210 | async deleteJob({ connectType, id }) {
211 | const store = await this.getStore()
212 | store.data = store.data.filter(item => {
213 | if (id) {
214 | return item.id !== id
215 | }
216 | if (connectType) {
217 | return item.connectType !== connectType || item.status !== this.status.completed.value
218 | }
219 | })
220 | return await localforage.setItem(this.storeKey, store)
221 | },
222 |
223 | async getStore() {
224 | const data = await localforage.getItem(this.storeKey)
225 | return data && data.version === this.initStore.version ? data : { ...this.initStore }
226 | },
227 | }
228 |
229 | export default Object.assign(Object.create(EventEmitter.prototype), Job)
230 |
--------------------------------------------------------------------------------
/src/renderer/api/message.js:
--------------------------------------------------------------------------------
1 | import message from 'iview/src/components/message'
2 |
3 | export default {
4 | info(config) {
5 | return message.info(config)
6 | },
7 | success(config) {
8 | return message.success(config)
9 | },
10 | warning(config) {
11 | if (typeof config === 'string')
12 | return message.warning({
13 | content: config,
14 | duration: 5,
15 | })
16 | if (typeof config === 'object')
17 | return message.warning({
18 | duration: 5,
19 | ...config,
20 | })
21 | },
22 | error(config) {
23 | if (typeof config === 'string')
24 | return message.error({
25 | content: config,
26 | duration: 5,
27 | })
28 | if (typeof config === 'object')
29 | return message.error({
30 | duration: 5,
31 | ...config,
32 | })
33 | },
34 | loading(config) {
35 | return message.loading(config)
36 | },
37 | }
38 |
--------------------------------------------------------------------------------
/src/renderer/api/notification.js:
--------------------------------------------------------------------------------
1 | import { winShow } from '@/api/electron'
2 |
3 | export default {
4 | notify(title = 'Updrive', options = {}, onClick) {
5 | const notify = new Notification(title, options)
6 |
7 | notify.onclick = (...arg) => {
8 | onClick(...arg)
9 | winShow()
10 | }
11 | },
12 | }
13 |
--------------------------------------------------------------------------------
/src/renderer/api/profile.js:
--------------------------------------------------------------------------------
1 | import localforage from 'localforage'
2 |
3 | const Profile = {
4 | initStore: {
5 | version: 0.2,
6 | data: {
7 | domain: '',
8 | urlCopyType: 'url',
9 | sortInfo: {
10 | isReverse: true,
11 | key: 'lastModified',
12 | },
13 | },
14 | },
15 |
16 | urlCopyType: {
17 | url: 'url',
18 | markdown: 'markdown',
19 | html: 'html',
20 | },
21 |
22 | setup(keyPre) {
23 | this.storeKey = `${keyPre}:profile`
24 | },
25 |
26 | async setStoreData(_profile) {
27 | const store = await this.getStore()
28 | store.data = { ...store.data, ..._profile }
29 | return await localforage.setItem(this.storeKey, store)
30 | },
31 |
32 | async getStore() {
33 | const store = await localforage.getItem(this.storeKey)
34 | if (!store || !store.version) return await localforage.setItem(this.storeKey, this.initStore)
35 | if (store.version !== this.initStore.version) return await this.upgrade(store)
36 | return store
37 | },
38 |
39 | async upgrade(store) {
40 | const data = { ...this.initStore.data }
41 | const oldData = { ...store.data }
42 | if (oldData.domain !== undefined) {
43 | data.domain = oldData.domain
44 | }
45 | if (oldData.urlCopyType !== undefined) {
46 | data.urlCopyType = oldData.urlCopyType
47 | }
48 | if (oldData.sortInfo !== undefined) {
49 | data.sortInfo = oldData.sortInfo
50 | }
51 | return await localforage.setItem(this.storeKey, { ...this.initStore, data })
52 | },
53 | }
54 |
55 | export default Profile
56 |
--------------------------------------------------------------------------------
/src/renderer/api/session.js:
--------------------------------------------------------------------------------
1 | export default {
2 | setUser(userInfo) {
3 | sessionStorage.setItem('currentUser', JSON.stringify(userInfo))
4 | },
5 | getUser() {
6 | let userInfo
7 | try {
8 | const _userInfo = sessionStorage.getItem('currentUser')
9 | if (_userInfo) userInfo = JSON.parse(_userInfo)
10 | } catch (err) {
11 | userInfo = null
12 | }
13 | return userInfo
14 | },
15 | clear() {
16 | sessionStorage.removeItem('currentUser')
17 | },
18 | }
19 |
--------------------------------------------------------------------------------
/src/renderer/api/thumnail.js:
--------------------------------------------------------------------------------
1 | export default {
2 | createThumbnail(originalUrl, targetWidth) {
3 |
4 | const onload = new Promise((resolve, reject) => {
5 | const image = new Image()
6 | image.src = originalUrl
7 | image.onload = () => {
8 | URL.revokeObjectURL(originalUrl)
9 | resolve(image)
10 | }
11 | image.onerror = () => {
12 | URL.revokeObjectURL(originalUrl)
13 | reject(new Error('Could not create thumbnail'))
14 | }
15 | })
16 |
17 | return onload
18 | .then(image => {
19 | const targetHeight = this.getProportionalHeight(image, targetWidth)
20 | const canvas = this.resizeImage(image, targetWidth, targetHeight)
21 | return this.canvasToBlob(canvas, 'image/png')
22 | })
23 | .then(blob => {
24 | if(blob) {
25 | return URL.createObjectURL(blob)
26 | } else {
27 | return ''
28 | }
29 | })
30 | },
31 |
32 | canvasToBlob(canvas, type, quality) {
33 | if (canvas.toBlob) {
34 | return new Promise(resolve => {
35 | canvas.toBlob(resolve, type, quality)
36 | })
37 | }
38 | return Promise.resolve().then(() => {
39 | return Utils.dataURItoBlob(canvas.toDataURL(type, quality), {})
40 | })
41 | },
42 |
43 | resizeImage(image, targetWidth, targetHeight) {
44 | // Resizing in steps refactored to use a solution from
45 | // https://blog.uploadcare.com/image-resize-in-browsers-is-broken-e38eed08df01
46 |
47 | image = this.protect(image)
48 |
49 | let steps = Math.ceil(Math.log2(image.width / targetWidth))
50 | if (steps < 1) {
51 | steps = 1
52 | }
53 | let sW = targetWidth * Math.pow(2, steps - 1)
54 | let sH = targetHeight * Math.pow(2, steps - 1)
55 | const x = 2
56 |
57 | while (steps--) {
58 | let canvas = document.createElement('canvas')
59 | canvas.width = sW
60 | canvas.height = sH
61 | canvas.getContext('2d').drawImage(image, 0, 0, sW, sH)
62 | image = canvas
63 |
64 | sW = Math.round(sW / x)
65 | sH = Math.round(sH / x)
66 | }
67 |
68 | return image
69 | },
70 |
71 | getProportionalHeight(img, width) {
72 | const aspect = img.width / img.height
73 | return Math.round(width / aspect)
74 | },
75 |
76 | protect (image) {
77 | // https://stackoverflow.com/questions/6081483/maximum-size-of-a-canvas-element
78 |
79 | var ratio = image.width / image.height
80 |
81 | var maxSquare = 5000000 // ios max canvas square
82 | var maxSize = 4096 // ie max canvas dimensions
83 |
84 | var maxW = Math.floor(Math.sqrt(maxSquare * ratio))
85 | var maxH = Math.floor(maxSquare / Math.sqrt(maxSquare * ratio))
86 | if (maxW > maxSize) {
87 | maxW = maxSize
88 | maxH = Math.round(maxW / ratio)
89 | }
90 | if (maxH > maxSize) {
91 | maxH = maxSize
92 | maxW = Math.round(ratio * maxH)
93 | }
94 | if (image.width > maxW) {
95 | var canvas = document.createElement('canvas')
96 | canvas.width = maxW
97 | canvas.height = maxH
98 | canvas.getContext('2d').drawImage(image, 0, 0, maxW, maxH)
99 | image.src = 'about:blank'
100 | image.width = 1
101 | image.height = 1
102 | image = canvas
103 | }
104 |
105 | return image
106 | }
107 |
108 | }
109 |
--------------------------------------------------------------------------------
/src/renderer/api/tool.js:
--------------------------------------------------------------------------------
1 | import { replace, compose, pipe, split, filter, reverse, sort, identity } from 'ramda'
2 | import Crypto from 'crypto'
3 | import Path from 'path'
4 | import { URL } from 'url'
5 | import Moment from 'moment'
6 | import { existsSync } from 'fs'
7 | import Message from '@/api/message'
8 |
9 | const userAgent = `${process.env.npm_package_build_productName}/${process.env.npm_package_version}`
10 |
11 | export const errorHandler = error => {
12 | if (error && error.response && error.response.data && error.response.data.msg) {
13 | Message.error(error.response.data.msg)
14 | } else {
15 | Message.error(error.message)
16 | }
17 | throw error
18 | }
19 |
20 | export const mandatory = parameter => {
21 | throw new Error(parameter ? `Missing parameter ${parameter}` : 'Missing parameter')
22 | }
23 |
24 | export const md5sum = data => {
25 | return Crypto.createHash('md5')
26 | .update(data, 'utf8')
27 | .digest('hex')
28 | }
29 |
30 | export const hmacSha1 = (secret = mandatory('secret'), data = mandatory('data')) => {
31 | return Crypto.createHmac('sha1', secret)
32 | .update(data, 'utf8')
33 | .digest()
34 | .toString('base64')
35 | }
36 |
37 | export const standardUri = (path = '') => {
38 | const pathStr = Array.isArray(path) ? path.join('/') : path
39 | return compose(replace(/(\/*)$/, '/'), replace(/^(\/*)/, '/'))(pathStr)
40 | }
41 |
42 | export const makeSign = ({
43 | method = mandatory('method'),
44 | uri = mandatory('uri'),
45 | date = mandatory('date'),
46 | passwordMd5 = mandatory('passwordMd5'),
47 | operatorName = mandatory('operatorName'),
48 | } = {}) => {
49 | return `UPYUN ${operatorName}:${hmacSha1(passwordMd5, [method, uri, date].join('&'))}`
50 | }
51 |
52 | // @TODO 实现 Content-MD5 校验
53 | export const getAuthorizationHeader = ({
54 | method = 'GET',
55 | url = '',
56 | passwordMd5 = mandatory('passwordMd5'),
57 | operatorName = mandatory('operatorName'),
58 | } = {}) => {
59 | const date = new Date().toGMTString()
60 |
61 | return {
62 | Authorization: makeSign({
63 | operatorName,
64 | passwordMd5,
65 | date,
66 | uri: new URL(url).pathname,
67 | method: method.toUpperCase(),
68 | }),
69 | 'x-date': date,
70 | }
71 | }
72 |
73 | export const base64 = (str = '') => new Buffer(str).toString('base64')
74 |
75 | // 以固定间隔时间立即执行的 throttle,和普通的不一样
76 | export const throttle = (fn, ms) => {
77 | let time = 0
78 | return (...args) => {
79 | const nowTime = +new Date()
80 | if (nowTime - time > ms) {
81 | time = nowTime
82 | fn(...args)
83 | }
84 | }
85 | }
86 |
87 | export const sleep = (ms = 0) => {
88 | return new Promise(r => setTimeout(r, ms))
89 | }
90 |
91 | export const isDir = (path = '') => {
92 | return /\/$/.test(path)
93 | }
94 |
95 | export const timestamp = (input, pattern = 'YYYY-MM-DD HH:mm:ss') =>
96 | isNaN(input) ? input : Moment.unix(input).format(pattern)
97 |
98 | export const digiUnit = input => {
99 | if (input === '-') return ''
100 | if (isNaN(input)) return input
101 | if (+input === 0) return '0 B'
102 | const getSizes = () => ['B', 'KB', 'MB', 'GB', 'TB']
103 | const getByte = input => Number(Math.abs(input))
104 | const getIndex = byte => Math.floor(Math.log(byte) / Math.log(1024))
105 | const getUnitIndex = (sizes = []) => index => (index > sizes.length - 1 ? sizes.length - 1 : index)
106 | const getResult = sizes => byte => index => `${(byte / Math.pow(1024, index)).toFixed(1)} ${sizes[index]}`
107 | return compose(
108 | compose(compose(getResult, getSizes)(), getByte)(input),
109 | compose(compose(getUnitIndex, getSizes)(), getIndex, getByte),
110 | )(input)
111 | }
112 |
113 | export const percent = (input, precision = 0) => {
114 | const num = parseFloat(input * 100, 10).toFixed(precision)
115 | return `${num} %`
116 | }
117 |
118 | export const uploadStatus = input => {
119 | return { '0': '未开始', '1': '进行中', '2': '已完成', '-1': '出错', '-2': '已取消' }[input]
120 | }
121 |
122 | // 递归获取不重复名字
123 | export const getLocalName = (fileName = '', init = true) => {
124 | if (!existsSync(fileName)) return fileName
125 | const match = /\((\d+)\)$/
126 | if (init && match.test(fileName)) {
127 | return getLocalName(fileName.replace(match, (match, p1) => `(${parseInt(p1) + 1})`), false)
128 | } else {
129 | return getLocalName(fileName + '(1)', false)
130 | }
131 | }
132 |
133 | // 获取文件类型
134 | export const getFileType = (IMME = '') => {
135 | const imagelist = ['image/gif', 'image/jpeg', 'image/png', 'image/svg+xml', 'image/bmp', 'image/webp']
136 | if (imagelist.includes(IMME.toLowerCase())) {
137 | return 'image'
138 | }
139 | }
140 |
141 | export const getFileTypeFromName = (filename = '', folderType) => {
142 | if (folderType === 'F' || folderType === 'B') return 'folder'
143 | const fileTypeMap = {
144 | image: ['.bmp', '.gif', '.ico', '.jpg', '.jpeg', '.png', '.svg', '.webp', '.gifv'],
145 | music: ['.mp3', '.m4a', '.ogg'],
146 | zip: ['.zip', '.rar', '.7z'],
147 | movie: ['.avi', '.mp4', '.flv', '.mov', '.3gp', '.asf', '.wmv', '.mpg', '.f4v', '.m4v', '.mkv'],
148 | html: ['.htm', '.html', '.vue'],
149 | json: ['.json'],
150 | javascript: ['.js', '.jsx'],
151 | style: ['.css', '.sass', '.less', '.stylus'],
152 | markdown: ['.md', '.markdown'],
153 | }
154 | const extensionName = Path.extname(filename).toLocaleLowerCase()
155 | return Object.keys(fileTypeMap).find(key => {
156 | return fileTypeMap[key].includes(extensionName)
157 | })
158 | }
159 |
160 | export const getFileIconClass = (filename = '', folderType) => {
161 | const filetype = getFileTypeFromName(filename, folderType)
162 | return `icon-${filetype}`
163 | }
164 |
165 | export const externalUrls = {
166 | repository: 'https://github.com/aniiantt/updrive',
167 | issues: 'https://github.com/aniiantt/updrive/issues/new',
168 | releases: 'https://github.com/aniiantt/updrive/releases',
169 | latest: 'https://github.com/aniiantt/updrive/releases/latest',
170 | }
171 |
172 | export const listSort = (data = [], key, isReverse) => {
173 | if (!key) return data
174 |
175 | const naturalCompareString = (a = '', b = '') => {
176 | try {
177 | const splitByNumber = pipe(split(/(\d+)/), filter(identity))
178 | const [aArr, bArr] = [splitByNumber(a), splitByNumber(b)]
179 | for (let i = 0; i < aArr.length; i++) {
180 | if (aArr[i] !== bArr[i]) {
181 | if (bArr[i] === undefined) return 1
182 | if (!isNaN(aArr[i]) && !isNaN(bArr[i])) {
183 | return parseInt(aArr[i]) - parseInt(bArr[i])
184 | } else {
185 | return aArr[i].localeCompare(bArr[i])
186 | }
187 | }
188 | }
189 | return 0
190 | } catch (err) {
191 | return a.localeCompare(b)
192 | }
193 | }
194 |
195 | const sortData = sort((ObjA, ObjB) => {
196 | if (ObjA.folderType !== ObjB.folderType) {
197 | if (!isReverse) return ObjA.folderType === 'F' ? -1 : 1
198 | if (isReverse) return ObjA.folderType === 'F' ? 1 : -1
199 | }
200 | if (key === 'lastModified' || key === 'size') {
201 | return ObjA[key] !== ObjB[key]
202 | ? Number(ObjA[key]) - Number(ObjB[key])
203 | : naturalCompareString(ObjA.filename, ObjB.filename)
204 | }
205 | if (key === 'filetype' || key === 'filename') {
206 | return ObjA[key] !== ObjB[key]
207 | ? naturalCompareString(String(ObjA[key]), String(ObjB[key]))
208 | : naturalCompareString(ObjA.filename, ObjB.filename)
209 | }
210 | }, data)
211 |
212 | return isReverse ? reverse(sortData) : sortData
213 | }
214 |
215 | export const createImage = () => {
216 |
217 | console.log('大大说')
218 | }
--------------------------------------------------------------------------------
/src/renderer/api/upyunClient.js:
--------------------------------------------------------------------------------
1 | import {
2 | tail,
3 | head,
4 | pipe,
5 | uniq,
6 | range,
7 | path,
8 | split,
9 | map,
10 | zipObj,
11 | compose,
12 | objOf,
13 | ifElse,
14 | isEmpty,
15 | assoc,
16 | replace,
17 | converge,
18 | always,
19 | prop,
20 | concat,
21 | identity,
22 | __,
23 | equals,
24 | } from 'ramda'
25 | import { readFileSync, createReadStream, createWriteStream, readdirSync, statSync, mkdirSync, existsSync } from 'fs'
26 | import Request from 'request'
27 | import Path from 'path'
28 | import mime from 'mime'
29 | import axios from 'axios'
30 |
31 | import { mandatory, base64, md5sum, sleep, isDir, getLocalName, getAuthorizationHeader } from '@/api/tool'
32 | import UpyunFtp from '@/api/upyunFtp'
33 |
34 | export default {
35 | bucketName: '',
36 | operatorName: '',
37 | passwordMd5: '',
38 | ftp: Object.create(UpyunFtp),
39 |
40 | setup(bucketName, operatorName, password) {
41 | this.bucketName = bucketName
42 | this.operatorName = operatorName
43 | this.passwordMd5 = md5sum(password)
44 | this.ftp.setup(bucketName, operatorName, password)
45 | },
46 |
47 | // // fetch 请求获取不了自定义响应头
48 | // requestWithFetch(input, config = {}, responseHandle = response => response) {
49 | // const url = this.getUrl(input)
50 | // config.headers = { ...config.headers, ...this.getHeaders(url, config.method) }
51 | // return window
52 | // .fetch(url, config)
53 | // .then(res => res.text())
54 | // .then(responseHandle)
55 | // },
56 |
57 | request(input, config = {}, responseHandle = response => response.data) {
58 | const url = this.getUrl(input)
59 | config.url = url
60 | config.headers = { ...config.headers, ...this.getHeaders(url, config.method) }
61 | return axios({
62 | responseType: 'text',
63 | ...config,
64 | }).then(responseHandle)
65 | },
66 |
67 | getUrl(input) {
68 | const uri = typeof input === 'object' ? input.uri : input
69 | const search = typeof input === 'object' ? input.search : ''
70 | const urlObject = new URL(`${this.bucketName}${uri}`, `https://v0.api.upyun.com`)
71 | if (search) urlObject.search = search
72 | return urlObject.href
73 | },
74 |
75 | getHeaders(url, method = 'GET') {
76 | return {
77 | ...getAuthorizationHeader({
78 | passwordMd5: this.passwordMd5,
79 | operatorName: this.operatorName,
80 | method: method,
81 | url,
82 | }),
83 | }
84 | },
85 |
86 | makeRequestOpts({ search = '', uri = '', method, headers = {} } = {}) {
87 | const url = this.getUrl(uri, { search })
88 |
89 | const _headers = { ...headers, ...this.getHeaders(url, method) }
90 |
91 | return {
92 | method,
93 | url,
94 | headers: _headers,
95 | }
96 | },
97 |
98 | // 遍历目录
99 | async traverseDir(uris = '', opts = {}) {
100 | let files = []
101 | // 递归遍历目录
102 | const parseDir = async (paths, fromPath = '') => {
103 | for (const _path of paths) {
104 | try {
105 | if (isDir(_path)) {
106 | files.push({
107 | absolutePath: _path,
108 | relativePath: fromPath + Path.basename(_path) + '/',
109 | })
110 | const dirData = await this.getListDirInfo(_path)
111 | if (dirData && dirData.data && dirData.data.length)
112 | await parseDir(dirData.data.map(fileObj => fileObj.uri), fromPath + Path.basename(_path) + '/')
113 | } else {
114 | files.push({
115 | absolutePath: _path,
116 | relativePath: fromPath + Path.basename(_path),
117 | })
118 | }
119 | } catch (err) {
120 | console.error(err)
121 | }
122 | }
123 | }
124 |
125 | await parseDir(uris)
126 |
127 | // 文件顺序
128 | if (opts.reverse === true) {
129 | files = files.reverse()
130 | }
131 |
132 | if (opts.type === 'file') {
133 | files = files.filter(f => !isDir(f.absolutePath))
134 | }
135 |
136 | if (opts.type === 'folder') {
137 | files = files.filter(f => isDir(f.absolutePath))
138 | }
139 |
140 | if (opts.relative !== true) {
141 | files = files.map(o => o.absolutePath)
142 | }
143 |
144 | return files
145 | },
146 |
147 |
148 | // HEAD 请求
149 | async head(uri) {
150 | return this.request(uri, { method: 'HEAD' }, response => response.headers)
151 | },
152 |
153 | // GET 请求
154 | async get(uri) {
155 | return this.request(uri, { method: 'GET' })
156 | },
157 |
158 | // 授权认证
159 | async checkAuth() {
160 | return this.request({ search: '?usage', uri: '/' })
161 | },
162 |
163 | // 获取使用量
164 | async getUsage() {
165 | return this.request({ search: '?usage', uri: '/' })
166 | },
167 |
168 | // 获取目录列表信息
169 | async getListDirInfo(uri = '/') {
170 | return this.request(uri, { method: 'GET' }).then(
171 | compose(
172 | assoc('path', uri),
173 | ifElse(
174 | isEmpty,
175 | () => ({ data: [] }),
176 | compose(
177 | objOf('data'),
178 | compose(
179 | map(obj => {
180 | obj.filetype = obj.folderType === 'F' ? '' : mime.getType(obj.filename)
181 | obj.uri = uri + obj.filename + (obj.folderType === 'F' ? '/' : '')
182 | return obj
183 | }),
184 | map(compose(zipObj(['filename', 'folderType', 'size', 'lastModified']), split(/\t/))),
185 | split(/\n/),
186 | ),
187 | ),
188 | ),
189 | ),
190 | )
191 | },
192 |
193 | // 创建目录
194 | async createFolder(location = '', folderName = '') {
195 | return this.request(`${location}${folderName}/`, { method: 'POST', headers: { folder: true } })
196 | },
197 |
198 | // 上传文件
199 | async uploadFiles(uri, localFilePaths = [], jobObj) {
200 | const results = []
201 |
202 | // 上传单个文件
203 | const uploadFile = async (uploadLocation, localFilePath) => {
204 | const localFileStat = statSync(localFilePath)
205 | const basename = Path.basename(localFilePath)
206 | if(!localFileStat.isFile()) return Promise.resolve(this.createFolder(uploadLocation, basename))
207 | const url = this.getUrl(uploadLocation + basename)
208 | const headers = { ...this.getHeaders(url, 'PUT') }
209 | return await jobObj.createUploadTask({
210 | url: url,
211 | headers: headers,
212 | localPath: localFilePath,
213 | })
214 | }
215 |
216 | // 广度优先遍历
217 | const uploadList = []
218 | let list = localFilePaths.slice().map(path => ({ localFilePath: path, relativePath: '' }))
219 |
220 | while (list.length) {
221 | const node = list.shift()
222 | const { localFilePath, relativePath } = node
223 | if (statSync(localFilePath).isDirectory() && readdirSync(localFilePath).length) {
224 | list = list.concat(
225 | readdirSync(localFilePath).map(name => ({
226 | localFilePath: Path.join(localFilePath, name),
227 | relativePath: relativePath + Path.basename(localFilePath) + '/',
228 | })),
229 | )
230 | } else {
231 | uploadList.push(node)
232 | }
233 | }
234 |
235 | for (const pathObj of uploadList) {
236 | const uploadLocation = uri + pathObj.relativePath
237 | try {
238 | results.push({
239 | result: true,
240 | location: uploadLocation,
241 | localPath: pathObj.localFilePath,
242 | message: await uploadFile(uploadLocation, pathObj.localFilePath),
243 | })
244 | } catch (err) {
245 | console.error(err)
246 | results.push({
247 | result: false,
248 | location: uploadLocation,
249 | localPath: pathObj.localFilePath,
250 | message: err && err.message,
251 | })
252 | }
253 | }
254 |
255 | return results
256 | },
257 |
258 | // 删除文件
259 | async deleteFiles(uris) {
260 | const results = []
261 | const waitDeleteInit = await this.traverseDir(uris, { reverse: true })
262 |
263 | for (const uri of waitDeleteInit) {
264 | try {
265 | results.push({
266 | uri: uri,
267 | result: true,
268 | message: await this.request(uri, { method: 'DELETE' }),
269 | })
270 | } catch (err) {
271 | results.push({
272 | uri: uri,
273 | result: false,
274 | message: err && err.message,
275 | })
276 | }
277 | }
278 |
279 | return results
280 | },
281 |
282 | // 下载文件
283 | async downloadFiles(destPath, uris, jobObj) {
284 | // 下载单个文件
285 | const downloadFile = async (localPath, uri) => {
286 | if (!uri && !existsSync(localPath)) return Promise.resolve(mkdirSync(localPath))
287 | const url = this.getUrl(uri)
288 | const headers = { ...this.getHeaders(url, 'GET') }
289 | return await jobObj.createDownloadTask({
290 | url: url,
291 | headers: headers,
292 | localPath: localPath,
293 | })
294 | }
295 |
296 | const results = []
297 |
298 | const dir = await this.traverseDir(uris, { relative: true })
299 | const dirAll = dir.map(pathObj => {
300 | return {
301 | uri: isDir(pathObj.absolutePath) ? '' : pathObj.absolutePath,
302 | localPath: Path.join(
303 | getLocalName(Path.join(destPath, pipe(prop('relativePath'), split('/'), head)(pathObj))),
304 | ...pipe(prop('relativePath'), split('/'), tail)(pathObj),
305 | ),
306 | }
307 | })
308 |
309 | for (const pathObj of dirAll) {
310 | try {
311 | results.push({
312 | uri: pathObj.uri,
313 | localPath: pathObj.localPath,
314 | result: true,
315 | message: await downloadFile(pathObj.localPath, pathObj.uri),
316 | })
317 | } catch (err) {
318 | results.push({
319 | uri: pathObj.uri,
320 | localPath: pathObj.localPath,
321 | result: false,
322 | message: err && err.message,
323 | })
324 | }
325 | }
326 |
327 | return results
328 | },
329 |
330 | // 重命名文件
331 | async renameFile(oldPath, newPath) {
332 | await this.ftp.renameFile(oldPath, newPath)
333 | },
334 | }
335 |
336 |
--------------------------------------------------------------------------------
/src/renderer/api/upyunFtp.js:
--------------------------------------------------------------------------------
1 | import {
2 | path,
3 | split,
4 | map,
5 | zipObj,
6 | compose,
7 | objOf,
8 | ifElse,
9 | isEmpty,
10 | assoc,
11 | replace,
12 | converge,
13 | always,
14 | prop,
15 | concat,
16 | identity,
17 | __,
18 | equals,
19 | } from 'ramda'
20 | import Moment from 'moment'
21 | import Ftp from 'ftp'
22 |
23 | export default {
24 | setup(bucketName, operatorName, password) {
25 | const ftpClient = new Ftp()
26 | ftpClient.on('ready', () => {
27 | console.info('--------------- ftp 连接成功 ---------------')
28 | })
29 | ftpClient.on('close', error => {
30 | console.info('--------------- ftp 已关闭 ---------------')
31 | })
32 |
33 | const connect = async () => {
34 | return new Promise((resolve, reject) => {
35 | ftpClient.connect({
36 | host: 'v0.ftp.upyun.com',
37 | user: `${operatorName}/${bucketName}`,
38 | password: password,
39 | })
40 | ftpClient.once('ready', resolve)
41 | })
42 | }
43 |
44 | const renamePromise = (oldPath, newPath) => {
45 | return new Promise(async (resolve, reject) => {
46 | ftpClient.rename(oldPath, newPath, err => {
47 | if (err) reject(err)
48 | resolve()
49 | })
50 | })
51 | }
52 |
53 | this.renameFile = async (oldPath, newPath) => {
54 | await connect()
55 | return renamePromise(oldPath, newPath)
56 | .then(() => {
57 | console.info('路径修改成功', `${oldPath} => ${newPath}`)
58 | ftpClient.end()
59 | return Promise.resolve(newPath)
60 | })
61 | .catch(err => {
62 | ftpClient.end()
63 | return Promise.reject(err)
64 | })
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/renderer/api/user.js:
--------------------------------------------------------------------------------
1 | import moment from 'moment'
2 | import localforage from 'localforage'
3 | import { remove, prepend } from 'ramda'
4 |
5 | import UpyunClient from '@/api/upyunClient'
6 |
7 | export default {
8 | storeKey: 'authHistory',
9 |
10 | initStore: {
11 | version: 0.1,
12 | data: [],
13 | },
14 |
15 | bucketName: '',
16 | operatorName: '',
17 | password: '',
18 | key: '',
19 | client: Object.create(UpyunClient),
20 |
21 | setup(bucketName, operatorName, password) {
22 | this.bucketName = bucketName
23 | this.operatorName = operatorName
24 | this.password = password
25 | this.key = `${this.operatorName}/${this.bucketName}`
26 | this.client.setup(this.bucketName, this.operatorName, this.password)
27 | },
28 |
29 | save() {
30 | this.getAuthHistory().then(data => {
31 | const authHistory = data
32 | const record = {
33 | bucketName: this.bucketName,
34 | operatorName: this.operatorName,
35 | password: this.password,
36 | key: this.key,
37 | lastModified: moment().unix(),
38 | remark: '',
39 | }
40 | const recordIndex = authHistory.data.findIndex(u => u.key === this.key)
41 | if (~recordIndex) {
42 | authHistory[recordIndex] = { ...record }
43 | } else {
44 | authHistory.data = prepend(record, authHistory.data)
45 | }
46 |
47 | return localforage.setItem(this.storeKey, authHistory)
48 | })
49 | },
50 |
51 | getAuthHistory() {
52 | return localforage.getItem(this.storeKey).then(data => {
53 | return data && data.version === this.initStore.version ? data : { ...this.initStore }
54 | })
55 | },
56 |
57 | deleteAuthHistory(key) {
58 | return this.getAuthHistory().then(data => {
59 | const authHistory = data
60 | authHistory.data = authHistory.data.filter(u => u.key !== key)
61 | return localforage.setItem(this.storeKey, authHistory)
62 | })
63 | },
64 | }
65 |
--------------------------------------------------------------------------------
/src/renderer/assets/icons/css.svg:
--------------------------------------------------------------------------------
1 |
2 |
55 |
--------------------------------------------------------------------------------
/src/renderer/assets/icons/file.svg:
--------------------------------------------------------------------------------
1 |
2 |
55 |
--------------------------------------------------------------------------------
/src/renderer/assets/icons/folder.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/renderer/assets/icons/html.svg:
--------------------------------------------------------------------------------
1 |
2 |
55 |
--------------------------------------------------------------------------------
/src/renderer/assets/icons/image.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/renderer/assets/icons/javascript.svg:
--------------------------------------------------------------------------------
1 |
2 |
55 |
--------------------------------------------------------------------------------
/src/renderer/assets/icons/json.svg:
--------------------------------------------------------------------------------
1 |
2 |
55 |
--------------------------------------------------------------------------------
/src/renderer/assets/icons/markdown.svg:
--------------------------------------------------------------------------------
1 |
2 |
55 |
--------------------------------------------------------------------------------
/src/renderer/assets/icons/movie.svg:
--------------------------------------------------------------------------------
1 |
2 |
55 |
--------------------------------------------------------------------------------
/src/renderer/assets/icons/music.svg:
--------------------------------------------------------------------------------
1 |
2 |
55 |
--------------------------------------------------------------------------------
/src/renderer/assets/icons/zip.svg:
--------------------------------------------------------------------------------
1 |
2 |
55 |
--------------------------------------------------------------------------------
/src/renderer/components/ConfirmModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
13 | {{content}}
14 |
15 |
16 |
20 |
21 |
22 |
23 |
24 |
68 |
69 |
80 |
--------------------------------------------------------------------------------
/src/renderer/components/Icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
21 |
--------------------------------------------------------------------------------
/src/renderer/components/LocalImage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
17 |
--------------------------------------------------------------------------------
/src/renderer/components/ProgressBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
53 |
--------------------------------------------------------------------------------
/src/renderer/components/ResIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
![]()
4 |
5 |
6 |
7 |
8 |
89 |
90 |
97 |
98 |
--------------------------------------------------------------------------------
/src/renderer/components/Spinner.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
35 |
--------------------------------------------------------------------------------
/src/renderer/fonts/SourceHanSansCN-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ntduan/updrive/8000819a68671e52e1ad802e0b50a686e5a3817e/src/renderer/fonts/SourceHanSansCN-Light.ttf
--------------------------------------------------------------------------------
/src/renderer/imgs/updrive.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/renderer/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import iviewCSS from 'iview/dist/styles/iview.css'
3 | import balloonCss from 'balloon-css/balloon.css'
4 |
5 | import App from '@/App'
6 | import Router from '@/router'
7 | import Store from '@/store'
8 |
9 | if (!process.env.IS_WEB) Vue.use(require('vue-electron'))
10 | Vue.config.productionTip = false
11 |
12 | /* eslint-disable no-new */
13 | new Vue({
14 | components: { App },
15 | router: Router,
16 | store: Store,
17 | template: '',
18 | }).$mount('#app')
19 |
--------------------------------------------------------------------------------
/src/renderer/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 | import { path } from 'ramda'
4 |
5 | import Store from '@/store'
6 |
7 | import Main from '@/views/layout/Main'
8 | import Login from '@/views/login/Login'
9 | import List from '@/views/list/List'
10 | import Upload from '@/views/upload/Upload'
11 | import Download from '@/views/download/Download'
12 | import Session from '@/api/session.js'
13 |
14 | Vue.use(Router)
15 |
16 | const router = new Router({
17 | routes: [
18 | {
19 | path: '/',
20 | component: Main,
21 | children: [
22 | {
23 | path: '',
24 | name: 'main',
25 | components: {
26 | body: List,
27 | },
28 | },
29 | {
30 | path: 'upload',
31 | name: 'upload',
32 | components: {
33 | body: Upload,
34 | },
35 | meta: {
36 | pageTitle: '上传列表',
37 | },
38 | },
39 | {
40 | path: 'download',
41 | name: 'download',
42 | components: {
43 | body: Download,
44 | },
45 | meta: {
46 | pageTitle: '下载列表',
47 | },
48 | },
49 | ],
50 | },
51 | {
52 | path: '/login',
53 | name: 'login',
54 | component: Login,
55 | },
56 | {
57 | path: '*',
58 | redirect: '/',
59 | },
60 | ],
61 | })
62 |
63 | router.beforeEach((to, from, next) => {
64 | const isLogined = path(['state', 'auth', 'isLogined'], Store)
65 |
66 | if (to.name === 'login' || isLogined) {
67 | next()
68 | } else {
69 | const userInfo = Session.getUser()
70 | if (userInfo) {
71 | Store.dispatch({
72 | type: 'VERIFICATION_ACCOUNT',
73 | ...userInfo,
74 | })
75 | .then(() => {
76 | next()
77 | })
78 | .catch(() => {
79 | next('/login')
80 | })
81 | } else {
82 | next('/login')
83 | }
84 | }
85 | })
86 |
87 | export default router
88 |
--------------------------------------------------------------------------------
/src/renderer/store/actions.js:
--------------------------------------------------------------------------------
1 | import { join, append, compose, unless, isEmpty } from 'ramda'
2 |
3 | import { errorHandler, getFileType } from '@/api/tool.js'
4 | import * as Types from '@/store/mutation-types'
5 | import * as UpyunFtp from '@/api/upyunFtp.js'
6 | import UpyunClient from '@/api/upyunClient.js'
7 | import Message from '@/api/message.js'
8 | import Session from '@/api/session.js'
9 | import Notification from '@/api/notification'
10 | import Router from '@/router'
11 |
12 | export default {
13 | // 登录
14 | [Types.VERIFICATION_ACCOUNT]({ getters, commit }, payload) {
15 | const userInfo = {
16 | bucketName: payload.bucketName,
17 | operatorName: payload.operatorName,
18 | password: payload.password,
19 | }
20 | commit(Types.SET_USER_INFO, userInfo)
21 |
22 | return getters.upyunClient
23 | .checkAuth()
24 | .then(() => {
25 | Session.setUser(userInfo)
26 | return userInfo
27 | })
28 | .catch(error => {
29 | commit(Types.CLEAR_USER_INFO)
30 | Session.clear()
31 | return Promise.reject(error)
32 | })
33 | },
34 | // 获取文件目录信息
35 | [Types.GET_LIST_DIR_INFO]({ getters, commit }, { remotePath, spinner = true, action }) {
36 | if (spinner) commit({ type: Types.SET_LOADING_LIST, data: true })
37 | return getters.upyunClient
38 | .getListDirInfo(remotePath)
39 | .then(result => {
40 | commit({
41 | type: Types.SET_CURRENT_LIST,
42 | data: result,
43 | action: action,
44 | })
45 | return result
46 | })
47 | .catch(errorHandler)
48 | },
49 | // 创建目录
50 | [Types.CREATE_FOLDER]({ getters, commit, dispatch }, { remotePath, folderName }) {
51 | return getters.upyunClient
52 | .createFolder(remotePath, folderName)
53 | .then(() => Message.success('文件夹创建成功'))
54 | .then(() => dispatch({ type: Types.REFRESH_LIST, spinner: false }))
55 | .catch(errorHandler)
56 | },
57 | // 刷新当前目录
58 | [Types.REFRESH_LIST]({ state, getters, commit, dispatch }, { remotePath, spinner = true } = {}) {
59 | return dispatch({ type: Types.GET_LIST_DIR_INFO, remotePath: remotePath || state.list.dirInfo.path, spinner })
60 | },
61 | // 删除文件
62 | [Types.DELETE_FILE]({ getters, commit, dispatch }, { selectedPaths } = {}) {
63 | return getters.upyunClient
64 | .deleteFiles(selectedPaths)
65 | .then(results => {
66 | const isAllSuccess = !results.some(r => !r.result)
67 | if (isAllSuccess) {
68 | Message.success('删除成功')
69 | } else {
70 | for (const result of results) Message.warning(`删除失败:${result.uri}: ${result.message}`)
71 | }
72 | })
73 | .then(() => dispatch({ type: Types.REFRESH_LIST, spinner: false }))
74 | .catch(errorHandler)
75 | },
76 | // 重命名
77 | [Types.RENAME_FILE]({ getters, commit, dispatch }, { oldPath, newPath, isFolder } = {}) {
78 | return getters.upyunClient
79 | .renameFile(oldPath, newPath)
80 | .then(() => Message.success('操作成功'))
81 | .then(() => dispatch({ type: Types.REFRESH_LIST, spinner: false }))
82 | .catch(errorHandler)
83 | },
84 | // 下载文件
85 | [Types.DOWNLOAD_FILES]({ getters, commit, dispatch }, { destPath, downloadPath } = {}) {
86 | return (
87 | getters.upyunClient
88 | .downloadFiles(destPath, downloadPath, getters.job)
89 | .then(results => {
90 | const isAllSuccess = !results.some(r => !r.result)
91 | const notify = title =>
92 | Notification.notify(
93 | title,
94 | {
95 | body: '点击查看详情',
96 | },
97 | () => {
98 | Router.push({ name: 'download' })
99 | },
100 | )
101 |
102 | if (isAllSuccess) {
103 | Message.success('下载成功')
104 | notify('下载成功')
105 | } else {
106 | for (const result of results) Message.warning(`下载失败:${result.uri}: ${result.message}`)
107 | notify('下载失败')
108 | }
109 | return dispatch(Types.SYNC_JOB_LIST)
110 | })
111 | .catch(errorHandler)
112 | // 同步错误信息
113 | .catch(() => dispatch(Types.SYNC_JOB_LIST))
114 | )
115 | },
116 | // 上传文件
117 | [Types.UPLOAD_FILES]({ getters, commit, dispatch }, { localFilePaths, remotePath } = {}) {
118 | return (
119 | getters.upyunClient
120 | .uploadFiles(remotePath, localFilePaths, getters.job)
121 | .then(results => {
122 | const isAllSuccess = !results.some(r => !r.result)
123 | const notify = title =>
124 | Notification.notify(
125 | title,
126 | {
127 | body: '点击查看详情',
128 | },
129 | () => {
130 | Router.push({ name: 'upload' })
131 | },
132 | )
133 | if (isAllSuccess) {
134 | Message.success('上传成功')
135 | notify('上传成功')
136 | } else {
137 | for (const result of results) Message.warning(`上传失败:${result.localPath}: ${result.message}`)
138 | notify('上传失败')
139 | }
140 | return dispatch(Types.SYNC_JOB_LIST)
141 | })
142 | .then(() => dispatch({ type: Types.REFRESH_LIST, spinner: false }))
143 | .catch(errorHandler)
144 | // 同步错误信息
145 | .catch(() => dispatch(Types.SYNC_JOB_LIST))
146 | )
147 | },
148 | // 获取文件详情信息
149 | [Types.GET_FILE_DETAIL_INFO]({ getters, commit }, { uri, basicInfo } = {}) {
150 | return Promise.resolve()
151 | .then(() => {
152 | if (basicInfo.folderType === 'F') return Promise.resolve()
153 | return getters.upyunClient.head(uri)
154 | })
155 | .then(data => {
156 | const fileType = data && getFileType(data['content-type'])
157 | commit({
158 | type: Types.SET_FILE_DETAIL_INFO,
159 | data: {
160 | headerInfo: data,
161 | fileType: fileType,
162 | basicInfo: basicInfo,
163 | },
164 | })
165 | })
166 | },
167 | // 同步任务列表
168 | [Types.SYNC_JOB_LIST]({ getters, commit }, {} = {}) {
169 | getters.job.getStore().then(store => {
170 | commit(Types.SET_JOB_LIST, store ? store.data : [])
171 | })
172 | },
173 | // 清空已完成任务
174 | [Types.DELETE_JOB]({ getters, commit, dispatch }, { connectType, id } = {}) {
175 | getters.job
176 | .deleteJob({ connectType, id })
177 | .then(() => {
178 | Message.success('操作成功')
179 | dispatch('SYNC_JOB_LIST')
180 | })
181 | .catch(errorHandler)
182 | },
183 | // 获取空间使用量
184 | [Types.GET_USAGE]({ state, getters, commit, dispatch }, {} = {}) {
185 | return getters.upyunClient.getUsage().then(data => {
186 | commit(Types.SET_USAGE, { data })
187 | })
188 | },
189 | // 退出登录
190 | [Types.LOGOUT]({ commit }) {
191 | const mutations = ['RESET_AUTH', 'RESET_LIST', 'RESET_MODAL', 'RESET_TASK']
192 | for (const m of mutations) commit(m)
193 | },
194 | // 设置 profile 存储数据
195 | [Types.SET_PROFILE_STORE]({ getters, dispatch }, { data } = {}) {
196 | getters.profile.setStoreData(data).then(() => {
197 | dispatch('SYNC_PROFILE_DATA')
198 | })
199 | },
200 | // 同步 profile 数据
201 | [Types.SYNC_PROFILE_DATA]({ getters, commit }, {} = {}) {
202 | getters.profile.getStore().then(store => {
203 | commit(Types.SET_PROFILE_DATA, store ? store.data : {})
204 | })
205 | },
206 | }
207 |
--------------------------------------------------------------------------------
/src/renderer/store/getters.js:
--------------------------------------------------------------------------------
1 | import { path, last } from 'ramda'
2 |
3 | import { externalUrls as __externalUrls } from '@/api/tool'
4 |
5 | export const bucketName = (state, getters) => {
6 | return path(['auth', 'user', 'bucketName'])(state) || ''
7 | }
8 |
9 | export const baseHref = (state, getters) => {
10 | return path(['profile', 'data', 'domain'])(state) || ''
11 | }
12 |
13 | export const externalUrls = (state, getters) => {
14 | return {
15 | domain: `https://console.upyun.com/services/${getters.bucketName}/domainsFile/`,
16 | createBucket: `https://console.upyun.com/services/create/file/`,
17 | ...__externalUrls,
18 | }
19 | }
20 |
21 | // upyunClient 对象
22 | export const upyunClient = state => {
23 | return path(['auth', 'user', 'client'], state) || null
24 | }
25 |
26 | // 获取 upyun api url
27 | export const getUpyunApiUrl = (state, getters) => uri => {
28 | return getters.upyunClient.getUrl(uri)
29 | }
30 |
31 | // job 对象
32 | export const job = state => {
33 | return path(['task', 'job'], state) || null
34 | }
35 |
36 | // profile handler 对象
37 | export const profile = state => {
38 | return path(['profile', 'handler'], state) || null
39 | }
40 |
--------------------------------------------------------------------------------
/src/renderer/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 |
4 | import Actions from '@/store/actions'
5 | import * as Getters from '@/store/getters'
6 | import Modules from '@/store/modules'
7 |
8 | Vue.use(Vuex)
9 |
10 | export default new Vuex.Store({
11 | actions: Actions,
12 | getters: Getters,
13 | modules: Modules,
14 | strict: process.env.NODE_ENV !== 'production',
15 | })
16 |
--------------------------------------------------------------------------------
/src/renderer/store/modules/auth.js:
--------------------------------------------------------------------------------
1 | import * as Types from '@/store/mutation-types'
2 | import { pickAll } from 'ramda'
3 |
4 | import User from '@/api/user'
5 |
6 | const user = Object.create(User)
7 |
8 | const initState = {
9 | user: user,
10 | usage: 0,
11 | isLogined: false,
12 | }
13 |
14 | const mutations = {
15 | [Types.SET_USER_INFO](state, payload = {}) {
16 | state.user.setup(payload.bucketName, payload.operatorName, payload.password)
17 | state.isLogined = true
18 | },
19 | [Types.SET_USAGE](state, { data }) {
20 | state.usage = data
21 | },
22 | [Types.CLEAR_USER_INFO](state) {
23 | sessionStorage.removeItem('key')
24 | Object.assign(state, { ...initState }, { user: Object.create(User) })
25 | },
26 | [Types.RESET_AUTH](state) {
27 | Object.assign(state, { ...initState }, { user: Object.create(User) })
28 | },
29 | }
30 |
31 | export default {
32 | state: { ...initState },
33 | mutations,
34 | }
35 |
--------------------------------------------------------------------------------
/src/renderer/store/modules/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * The file enables `@/store/index.js` to import all vuex modules
3 | * in a one-shot manner. There should not be any reason to edit this file.
4 | */
5 |
6 | const files = require.context('.', false, /\.js$/)
7 | const modules = {}
8 |
9 | files.keys().forEach(key => {
10 | if (key === './index.js') return
11 | modules[key.replace(/(\.\/|\.js)/g, '')] = files(key).default
12 | })
13 |
14 | export default modules
15 |
--------------------------------------------------------------------------------
/src/renderer/store/modules/list.js:
--------------------------------------------------------------------------------
1 | import {
2 | last,
3 | dropLast,
4 | path,
5 | merge,
6 | append,
7 | pluck,
8 | } from 'ramda'
9 | import * as Types from '@/store/mutation-types'
10 |
11 | const initState = {
12 | dirInfo: {
13 | data: [],
14 | loading: false,
15 | path: '',
16 | },
17 | history: {
18 | forwardStack: [],
19 | backStack: [],
20 | },
21 | fileDetail: {
22 | basicInfo: {},
23 | headerInfo: {},
24 | },
25 | selected: [],
26 | }
27 |
28 |
29 |
30 | const mutations = {
31 | [Types.SET_CURRENT_LIST](state, { data, action }) {
32 | const historyPath = path(['dirInfo', 'path'], state)
33 | let forwardStack = path(['history', 'forwardStack'], state)
34 | let backStack = path(['history', 'backStack'], state)
35 |
36 | state.dirInfo = {
37 | ...data,
38 | loading: false,
39 | }
40 |
41 | state.selected = []
42 |
43 | // action 0 表示打开新目录
44 | if (action === 0) {
45 | state.history.forwardStack = []
46 | if (last(state.history.backStack) !== historyPath) {
47 | state.history.backStack = append(historyPath, backStack)
48 | }
49 | }
50 | // action 1 表示前进
51 | if (action === 1) {
52 | if (state.history.forwardStack.length) {
53 | state.history.backStack = append(historyPath, backStack)
54 | state.history.forwardStack = dropLast(1, forwardStack)
55 | }
56 | }
57 | // action -1 表示后退
58 | if (action === -1) {
59 | if (state.history.backStack.length) {
60 | state.history.forwardStack = append(historyPath, forwardStack)
61 | state.history.backStack = dropLast(1, backStack)
62 | }
63 | }
64 | },
65 | [Types.SET_LOADING_LIST](state, { data }) {
66 | state.dirInfo.loading = data
67 | },
68 | [Types.SHORTCUT_SELECT_ALL](state, data) {
69 | state.selected = pluck('uri', state.dirInfo.data)
70 | },
71 | [Types.SET_SELECT_LIST](state, { selected }) {
72 | state.selected = selected
73 | },
74 | [Types.SET_FILE_DETAIL_INFO](state, { data }) {
75 | state.fileDetail = data
76 | },
77 | [Types.RESET_LIST](state) {
78 | Object.assign(state, { ...initState })
79 | },
80 | }
81 |
82 | export default {
83 | state: { ...initState },
84 | mutations,
85 | }
86 |
--------------------------------------------------------------------------------
/src/renderer/store/modules/modal.js:
--------------------------------------------------------------------------------
1 | import * as Types from '@/store/mutation-types'
2 |
3 | const initState = {
4 | createFolder: {
5 | show: false,
6 | },
7 | renameFile: {
8 | show: false,
9 | oldPath: '',
10 | },
11 | domainSetting: {
12 | show: false,
13 | },
14 | formatUrl: {
15 | show: false,
16 | url: '',
17 | },
18 | uploadHandle: {
19 | show: false,
20 | },
21 | }
22 |
23 | const mutations = {
24 | [Types.OPEN_UPLOAD_HANDLE_MODAL](state, { data }) {
25 | state.uploadHandle.show = true
26 | },
27 | [Types.CLOSE_UPLOAD_HANDLE_MODAL](state) {
28 | state.uploadHandle.show = false
29 | },
30 | [Types.OPEN_FORMAT_URL_MODAL](state, { data }) {
31 | state.formatUrl.show = true
32 | state.formatUrl.url = data
33 | },
34 | [Types.CLOSE_FORMAT_URL_MODAL](state) {
35 | state.formatUrl.show = false
36 | state.formatUrl.url = ''
37 | },
38 | [Types.OPEN_CREATE_FOLDER_MODAL](state) {
39 | state.createFolder.show = true
40 | },
41 | [Types.CLOSE_CREATE_FOLDER_MODAL](state) {
42 | state.createFolder.show = false
43 | },
44 | [Types.OPEN_DOMAIN_SETTING_MODAL](state) {
45 | state.domainSetting.show = true
46 | },
47 | [Types.CLOSE_DOMAIN_SETTING_MODAL](state) {
48 | state.domainSetting.show = false
49 | },
50 | [Types.OPEN_RENAME_FILE_MODAL](state) {
51 | state.renameFile.show = true
52 | },
53 | [Types.CLOSE_RENAME_FILE_MODAL](state) {
54 | state.renameFile.show = false
55 | },
56 | [Types.RENAME_FILE_SET_OLD_PATH](state, oldPath) {
57 | state.renameFile.oldPath = oldPath
58 | },
59 | [Types.RENAME_FILE_CLEAR_OLD_PATH](state) {
60 | state.renameFile.oldPath = ''
61 | },
62 | [Types.RESET_MODAL](state) {
63 | Object.assign(state, { ...initState })
64 | },
65 | }
66 |
67 | export default {
68 | state: { ...initState },
69 | mutations,
70 | }
71 |
--------------------------------------------------------------------------------
/src/renderer/store/modules/profile.js:
--------------------------------------------------------------------------------
1 | import * as Types from '@/store/mutation-types'
2 |
3 | const initState = {
4 | handler: null,
5 | data: {},
6 | }
7 |
8 | const mutations = {
9 | [Types.INIT_PROFILE](state, handler) {
10 | state.handler = handler
11 | },
12 | [Types.SET_PROFILE_DATA](state, data) {
13 | state.data = data
14 | },
15 | [Types.RESET_PROFILE](state) {
16 | Object.assign(state, { ...initState })
17 | },
18 | }
19 |
20 | export default {
21 | state: { ...initState },
22 | mutations,
23 | }
24 |
--------------------------------------------------------------------------------
/src/renderer/store/modules/task.js:
--------------------------------------------------------------------------------
1 | import { append, drop, prepend, update } from 'ramda'
2 |
3 | import * as Types from '@/store/mutation-types'
4 |
5 | const initState = {
6 | taskType: {
7 | upload: '上传',
8 | download: '下载',
9 | },
10 | status: {},
11 | job: null,
12 | list: [],
13 | taskList: [],
14 | showModal: false,
15 | }
16 |
17 | const mutations = {
18 | [Types.INIT_JOB](state, job) {
19 | state.job = job
20 | state.status = job.status
21 | },
22 | [Types.SET_JOB_LIST](state, list) {
23 | state.list = list
24 | },
25 | [Types.UPDATE_JOB_ITEM](state, { item }) {
26 | const existedItemIndex = state.list.findIndex(_item => _item.id === item.id)
27 | if (~existedItemIndex) {
28 | state.list = update(existedItemIndex, item, state.list)
29 | } else {
30 | state.list = prepend(item, state.list)
31 | }
32 | },
33 | [Types.SHOW_TASK_MODAL](state) {
34 | state.showModal = true
35 | },
36 | [Types.HIDE_TASK_MODAL](state) {
37 | state.taskList = state.taskList.filter(item => {
38 | return item.status === '1'
39 | })
40 | state.showModal = false
41 | },
42 | [Types.RESET_TASK](state) {
43 | Object.assign(state, { ...initState })
44 | },
45 | }
46 |
47 | export default {
48 | state: { ...initState },
49 | mutations,
50 | }
51 |
--------------------------------------------------------------------------------
/src/renderer/store/mutation-types.js:
--------------------------------------------------------------------------------
1 | // mutations
2 |
3 | export const CLEAR_USER_INFO = 'CLEAR_USER_INFO' // 清除用户信息
4 |
5 |
6 | export const RESET_AUTH = 'RESET_AUTH'
7 | export const RESET_LIST = 'RESET_LIST'
8 | export const RESET_MODAL = 'RESET_MODAL'
9 | export const RESET_TASK = 'RESET_TASK'
10 | export const RESET_PROFILE = 'RESET_PROFILE'
11 |
12 | export const LOGIN_SUCCESS = 'LOGIN_SUCCESS' // 登录成功
13 | export const LOGOUT = 'LOGOUT' // 退出登录
14 | export const SET_USER_INFO = 'SET_USER_INFO' // 设置用户信息
15 | export const SET_LOADING_LIST = 'SET_LOADING_LIST' // 目录正在加载中
16 | export const SET_CURRENT_LIST = 'SET_CURRENT_LIST' // 设置当前目录列表
17 | export const SET_SORT_INFO = 'SET_SORT_INFO' // 排序key
18 | export const CHANGE_DIR = 'CHANGE_DIR' // 改变当前目录
19 | export const SHORTCUT_SELECT_ALL = 'SHORTCUT_SELECT_ALL' // 选择所有文件
20 | export const SET_SELECT_LIST = 'SET_SELECT_LIST' // 选择所有文件
21 |
22 | export const OPEN_CREATE_FOLDER_MODAL = 'OPEN_CREATE_FOLDER_MODAL' // 打开创建文件夹 modal
23 | export const CLOSE_CREATE_FOLDER_MODAL = 'CLOSE_CREATE_FOLDER_MODAL' // 关闭创建文件夹 modal
24 |
25 | export const OPEN_RENAME_FILE_MODAL = 'OPEN_RENAME_FILE_MODAL' // 打开重命名 modal
26 | export const CLOSE_RENAME_FILE_MODAL = 'CLOSE_RENAME_FILE_MODAL' // 关闭重命名 modal
27 |
28 | export const RENAME_FILE_SET_OLD_PATH = 'RENAME_FILE_SET_OLD_PATH' // 设置 oldpath
29 | export const RENAME_FILE_CLEAR_OLD_PATH = 'RENAME_FILE_CLEAR_OLD_PATH' // 清除 oldpath
30 |
31 | export const OPEN_DOMAIN_SETTING_MODAL = 'OPEN_DOMAIN_SETTING_MODAL' // 打开域名设置框
32 | export const CLOSE_DOMAIN_SETTING_MODAL = 'CLOSE_DOMAIN_SETTING_MODAL' // 关闭域名设置框
33 |
34 | export const OPEN_UPLOAD_HANDLE_MODAL = 'OPEN_UPLOAD_HANDLE_MODAL' // 打开上传操作框
35 | export const CLOSE_UPLOAD_HANDLE_MODAL = 'CLOSE_UPLOAD_HANDLE_MODAL' // 关闭上传操作框
36 |
37 | export const OPEN_FORMAT_URL_MODAL = 'OPEN_FORMAT_URL_MODAL' // 打开链接格式化框
38 | export const CLOSE_FORMAT_URL_MODAL = 'CLOSE_FORMAT_URL_MODAL' // 关闭链接格式化框
39 | export const SET_FORMAT_TYPE = 'SET_FORMAT_TYPE' // 设置链接的格式
40 | export const SET_COPY_URL = 'SET_COPY_URL' // 设置需要复制的URL
41 |
42 | export const SHOW_TASK_MODAL = 'SHOW_TASK_MODAL' // 显示任务框
43 | export const HIDE_TASK_MODAL = 'HIDE_TASK_MODAL' // 隐藏任务框
44 |
45 | export const INIT_JOB = 'INIT_JOB' // 初始化任务实例
46 | export const SET_JOB_LIST = 'SET_JOB_LIST' // 设置任务列表
47 | export const UPDATE_JOB_ITEM = 'UPDATE_JOB_ITEM' // 更新一个任务
48 | export const DELETE_JOB = 'DELETE_JOB' // 删除任务
49 | export const SYNC_JOB_LIST = 'SYNC_JOB_LIST' // 同步下载任务列表
50 |
51 | // actions
52 | export const VERIFICATION_ACCOUNT = 'VERIFICATION_ACCOUNT' // 验证账号
53 | export const GET_LIST_DIR_INFO = 'GET_LIST_DIR_INFO' // 获取列表信息
54 | export const UPLOAD_FILES = 'UPLOAD_FILES' // 上传文件
55 | export const CREATE_FOLDER = 'CREATE_FOLDER' // 创建文件夹
56 | export const REFRESH_LIST = 'REFRESH_LIST' // 刷新当前目录
57 | export const DELETE_FILE = 'DELETE_FILE' // 删除文件
58 | export const RENAME_FILE = 'RENAME_FILE' // 重命名文件
59 | export const DOWNLOAD_FILES = 'DOWNLOAD_FILES' // 下载文件
60 | export const GET_FILE_DETAIL_INFO = 'GET_FILE_DETAIL_INFO' // 获取文件详情信息
61 | export const SET_FILE_DETAIL_INFO = 'SET_FILE_DETAIL_INFO' // 设置文件详情信息
62 |
63 | export const INIT_PROFILE = 'INIT_PROFILE' // 初始化 profile
64 | export const SET_PROFILE_STORE = 'SET_PROFILE_STORE' // 设置 profile 存储数据
65 | export const SET_PROFILE_DATA = 'SET_PROFILE_DATA' // 同步 profile 数据
66 | export const SYNC_PROFILE_DATA = 'SYNC_PROFILE_DATA' // 设置 profile 数据
67 |
68 | export const SET_USAGE = 'SET_USAGE' // 设置空间使用量
69 | export const GET_USAGE = 'GET_USAGE' // 获取空间使用量
70 |
--------------------------------------------------------------------------------
/src/renderer/styles/bulma.scss:
--------------------------------------------------------------------------------
1 | @import '~bulma/sass/utilities/initial-variables';
2 | @import '~@/styles/custom.scss';
3 |
4 | @font-face {
5 | font-family: SourceHanSansCN-Light;
6 | src: url('./fonts/SourceHanSansCN-Light.ttf');
7 | font-weight: normal;
8 | }
9 |
10 | $family-sans-serif: 'Arial', 'SourceHanSansCN-Light', 'Microsoft YaHei', BlinkMacSystemFont, -apple-system, 'Segoe UI',
11 | 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Helvetica', sans-serif;
12 |
13 | $family-primary: $family-sans-serif;
14 |
15 | $primary: $upyunPrimaryColor;
16 |
17 | $menu-item-hover-background-color: #edf4fc;
18 | $menu-item-color: #000;
19 | $menu-item-active-background-color: #cce8ff;
20 | $menu-item-active-color: #000;
21 |
22 | $dropdown-item-hover-background-color: #edf4fc;
23 | @import '~bulma/bulma.sass';
24 |
25 | .menu {
26 | color: findColorInvert(#000);
27 | }
28 |
29 | .message a.message-link:not(.button):not(.tag) {
30 | color: #3273dc;
31 | &:hover {
32 | color: #363636;
33 | }
34 | }
35 |
36 | .message {
37 | user-select: auto;
38 | }
39 |
40 | .content {
41 | user-select: auto;
42 | }
43 |
--------------------------------------------------------------------------------
/src/renderer/styles/common.scss:
--------------------------------------------------------------------------------
1 | html {
2 | overflow-y: hidden;
3 | height: 100%;
4 | font-size: 14px;
5 | }
6 |
7 | body {
8 | height: 100%;
9 | display: flex;
10 | flex-direction: column;
11 | user-select: none;
12 | }
13 |
14 | .modal-footer .button {
15 | padding-left: 20px;
16 | padding-right: 20px;
17 | }
18 |
19 | .svg-icon {
20 | width: 1em;
21 | height: 1em;
22 | vertical-align: -0.15em;
23 | fill: currentColor;
24 | overflow: hidden;
25 | }
26 |
27 | .login-form {
28 | .label {
29 | margin-bottom: 0;
30 | }
31 | }
32 |
33 | .drag-over {
34 | background-color: #f5fbff;
35 | }
36 |
37 | /*
38 | * 覆盖 bulma
39 | */
40 |
41 | .breadcrumb li.is-active a {
42 | cursor: pointer;
43 | pointer-events: inherit;
44 | }
45 |
46 | .breadcrumb a {
47 | padding: 0.5em 0.5em;
48 | display: block;
49 | overflow: hidden;
50 | text-overflow: ellipsis;
51 | max-width: 150px;
52 | }
53 |
54 | .breadcrumb > ul {
55 | padding-left: 15px;
56 | }
57 |
58 | /*
59 | * 覆盖 balloon-css
60 | */
61 |
62 | [data-balloon]:after {
63 | font-family: inherit !important;
64 | }
65 |
66 | /*
67 | * 进度条提示
68 | */
69 |
70 | .file-progress {
71 | position: absolute;
72 | width: 360px;
73 | right: 12px;
74 | font-size: 12px;
75 | bottom: 0;
76 | z-index: 10;
77 | box-shadow: 0 2px 8px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1);
78 | .card-block {
79 | cursor: pointer;
80 | .card-block-title {
81 | overflow: hidden;
82 | white-space: nowrap;
83 | text-overflow: ellipsis;
84 | flex-grow: 1;
85 | }
86 | .card-block-icon {
87 | padding-right: 8px;
88 | padding-left: 8px;
89 | }
90 | }
91 | .progress-bar {
92 | width: 20px;
93 | height: 20px;
94 | }
95 | }
96 |
97 | .card-block {
98 | display: flex;
99 | align-items: center;
100 | height: 50px;
101 | padding: 0.5em 0.7em;
102 | border-bottom: 1px solid rgba(0, 0, 0, 0.12);
103 | }
104 |
105 | .account-history {
106 | .account-history-list {
107 | max-height: 320px;
108 | overflow: auto;
109 | }
110 | .record {
111 | cursor: default;
112 | &:hover .record-delete {
113 | display: block;
114 | }
115 | .record-delete {
116 | &:hover {
117 | text-decoration: underline;
118 | }
119 | color: #3273dc;
120 | display: none;
121 | cursor: pointer;
122 | float: right;
123 | font-size: 12px;
124 | line-height: 18px;
125 | }
126 | }
127 | }
128 |
129 |
130 | .dropdown-background {
131 | z-index: 1;
132 | position: fixed;
133 | top: 0;
134 | bottom: 0;
135 | left: 0;
136 | right: 0;
137 | }
138 |
--------------------------------------------------------------------------------
/src/renderer/styles/custom.scss:
--------------------------------------------------------------------------------
1 | $upyunPrimaryColor: #00A0FF;
2 | // $upyunPrimaryColor: #15bdf9;
3 | $upyunDisabledColor: #7fd0ff;
--------------------------------------------------------------------------------
/src/renderer/styles/icons.scss:
--------------------------------------------------------------------------------
1 | .res-icon {
2 | vertical-align: -7px;
3 | font-size: 24px;
4 | display: inline-block;
5 | line-height: 1;
6 | width: 24px;
7 | height: 24px;
8 | text-align: center;
9 | margin-right: 5px;
10 | &:before {
11 | content: url("assets/icons/file.svg");
12 | }
13 | &.icon-folder {
14 | &:before {
15 | content: url("assets/icons/folder.svg");
16 | }
17 | }
18 | &.icon-image {
19 | &:before {
20 | content: url("assets/icons/image.svg");
21 | }
22 | }
23 | &.icon-html {
24 | &:before {
25 | content: url("assets/icons/html.svg");
26 | }
27 | }
28 | &.icon-javascript {
29 | &:before {
30 | content: url("assets/icons/javascript.svg");
31 | }
32 | }
33 | &.icon-style {
34 | &:before {
35 | content: url("assets/icons/css.svg");
36 | }
37 | }
38 | &.icon-json {
39 | &:before {
40 | content: url("assets/icons/json.svg");
41 | }
42 | }
43 | &.icon-markdown {
44 | &:before {
45 | content: url("assets/icons/markdown.svg");
46 | }
47 | }
48 | &.icon-music {
49 | &:before {
50 | content: url("assets/icons/music.svg");
51 | }
52 | }
53 | &.icon-movie {
54 | &:before {
55 | content: url("assets/icons/movie.svg");
56 | }
57 | }
58 | &.icon-zip {
59 | &:before {
60 | content: url("assets/icons/zip.svg");
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/renderer/styles/index.scss:
--------------------------------------------------------------------------------
1 | @import "~@/styles/bulma.scss";
2 | @import "~@/styles/common.scss";
3 | @import "~@/styles/scroll.scss";
4 | @import "~@/styles/icons.scss";
5 | @import "~@/styles/layout.scss";
6 | @import "~@/styles/listView.scss";
7 | @import "~@/styles/modal.scss";
8 | @import "~@/styles/task.scss";
--------------------------------------------------------------------------------
/src/renderer/styles/layout.scss:
--------------------------------------------------------------------------------
1 | .layout {
2 | display: flex;
3 | flex-direction: column;
4 | height: 100%;
5 | width: 100%;
6 | overflow: hidden;
7 | background: #fcfcfc;
8 | .layout-inner {
9 | display: flex;
10 | flex: 1;
11 | }
12 | .layout-body {
13 | display: flex;
14 | flex: 1;
15 | }
16 | }
17 |
18 | .profile {
19 | cursor: pointer;
20 | position: relative;
21 | color: rgba(0, 0, 0, 0.8);
22 | height: 65px;
23 | padding-left: 20px;
24 | &:hover {
25 | background: #edf4fc;
26 | }
27 |
28 | .dropdown-content {
29 | cursor: default;
30 | position: absolute;
31 | font-size: 18px;
32 | top: 39px;
33 | z-index: 1;
34 | width: 240px;
35 | padding: 16px;
36 | .dropdown-item {
37 | cursor: pointer;
38 | }
39 | }
40 | }
41 |
42 | .profile-name {
43 | display: flex;
44 | align-items: center;
45 | line-height: 39px;
46 | color: rgba(0, 0, 0, 0.8);
47 | font-size: 16px;
48 | }
49 |
50 | .profile-name-content {
51 | white-space: nowrap;
52 | overflow: hidden;
53 | text-overflow: ellipsis;
54 | }
55 |
56 | .profile-usage {
57 | color: rgba(0, 0, 0, 0.4);
58 | font-size: 12px;
59 | }
60 |
61 | .menu {
62 | display: flex;
63 | flex-direction: column;
64 | color: rgba(0, 0, 0, 0.8);
65 | width: 160px;
66 | background: #ffffff;
67 | border-right: 1px solid rgba(0, 0, 0, 0.12);
68 | .icon-angle-down.svg-icon {
69 | color: rgba(0, 0, 0, 0.54);
70 | width: 10px;
71 | height: 10px;
72 | margin: 4px 8px 0 4px;
73 | }
74 | .menu-list {
75 | flex: 1;
76 | .svg-icon {
77 | margin-right: 5px;
78 | width: 1.3em;
79 | height: 1.3em;
80 | vertical-align: -0.25em;
81 | }
82 | a {
83 | padding: 8px 20px;
84 | padding-right: 8px;
85 | }
86 | }
87 | }
88 |
89 | .dropdown-content-profile-name {
90 | padding: 0.375rem 1rem;
91 | word-break: break-all;
92 | line-height: 1;
93 | }
94 | // page-header
95 | .page-header {
96 | align-items: center;
97 | display: flex;
98 | padding: 0 12px;
99 | background: #fff;
100 | }
101 |
102 | .page-title {
103 | font-size: 1.5em;
104 | line-height: 1;
105 | margin: 15px 0;
106 | }
107 |
108 | .logo {
109 | line-height: 50px;
110 | display: inline-block;
111 | }
112 |
113 | .logo img {
114 | height: 25px;
115 | vertical-align: middle;
116 | }
117 |
118 | // page-nav
119 | .bar {
120 | align-items: center;
121 | display: flex;
122 | z-index: 1;
123 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
124 | background: #fafafa;
125 | .bar-right {
126 | display: flex;
127 | font-size: 24px;
128 | margin-left: 0.75em;
129 | margin-right: 0.75em;
130 | }
131 | }
132 |
133 | .button-zone {
134 | width: 160px;
135 | display: flex;
136 | justify-content: center;
137 | .dropdown-trigger .button {
138 | width: 100px;
139 | position: relative;
140 | }
141 | }
142 |
143 | .nav {
144 | align-items: center;
145 | background: #fafafa;
146 | flex: 1;
147 | padding-right: 16px;
148 | font-size: 1.5em;
149 | overflow: hidden;
150 | }
151 |
152 | .task-tag.tag {
153 | font-size: 12px;
154 | height: 16px;
155 | line-height: 16px;
156 | padding: 0 8px;
157 | margin-left: 8px;
158 | transform: translateY(-1px);
159 | }
160 |
161 | .app-info {
162 | display: flex;
163 | align-items: center;
164 | justify-content: center;
165 | color: rgba(0, 0, 0, 0.12);
166 | padding: 8px 8px;
167 | font-size: 12px;
168 | cursor: pointer;
169 | &:hover {
170 | background-color: #edf4fc;
171 | }
172 | .app-upgrade-tip {
173 | width: 5px;
174 | height: 5px;
175 | border-radius: 50%;
176 | background-color: #ff8a80;
177 | margin-bottom: 10px;
178 | margin-left: 2px;
179 | }
180 | }
181 |
182 | .about-modal {
183 | .modal-content {
184 | width: 480px;
185 | }
186 | .brand-block {
187 | display: flex;
188 | align-items: center;
189 | flex-direction: column;
190 | .brand-img img {
191 | width: 48px;
192 | height: 48px;
193 | }
194 | .brand-name {
195 | font-size: 20px;
196 | color: rgba(0, 0, 0, 0.84);
197 | }
198 | .brand-version {
199 | display: flex;
200 | align-items: center;
201 | font-size: 16px;
202 | margin-bottom: 16px;
203 | .button {
204 | padding: 0 4px;
205 | margin-left: 4px;
206 | font-size: 12px;
207 | height: 18px;
208 | }
209 | }
210 | padding-bottom: 15px;
211 | }
212 | .change-logs {
213 | display: flex;
214 | flex-direction: column;
215 | padding-top: 15px;
216 | border-top: 1px solid rgba(0, 0, 0, 0.08);
217 | }
218 | .change-logs-content {
219 | h3 {
220 | font-size: 16px;
221 | }
222 | ul {
223 | font-size: 12px;
224 | }
225 | .version-message:not(:last-child) {
226 | margin-bottom: 16px;
227 | }
228 | }
229 | .message-body {
230 | max-height: 224px;
231 | overflow-y: scroll;
232 | }
233 | .upgrade-download-tip {
234 | font-size: 12px;
235 | margin-bottom: 8px;
236 | }
237 | }
238 |
--------------------------------------------------------------------------------
/src/renderer/styles/listView.scss:
--------------------------------------------------------------------------------
1 | @import '~@/styles/custom.scss';
2 |
3 | @media (max-width: 1200px) {
4 | .hide-2000 {
5 | display: none !important;
6 | }
7 | }
8 |
9 | .list-view {
10 | flex: 1;
11 | display: flex;
12 | .list-view-main {
13 | position: relative;
14 | display: flex;
15 | flex: 1;
16 | flex-direction: column;
17 | background: #fff;
18 | outline: none;
19 | }
20 | .list-view-detail {
21 | width: 350px;
22 | background: #fff;
23 | position: relative;
24 | user-select: auto;
25 | box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.14);
26 | border-left: 1px solid rgba(0, 0, 0, 0.12);
27 | .list-view-detail-header {
28 | padding-left: 10px;
29 | padding-right: 10px;
30 | .res-icon {
31 | margin-right: 0;
32 | }
33 | h4 {
34 | width: 275px;
35 | font-size: 16px;
36 | line-height: 39px;
37 | overflow: hidden;
38 | text-overflow: ellipsis;
39 | white-space: nowrap;
40 | }
41 | }
42 | .separate-line-wrap {
43 | margin-right: -10px;
44 | margin-left: -10px;
45 | }
46 | .list-view-detail-close {
47 | cursor: pointer;
48 | opacity: 0.5;
49 | position: absolute;
50 | height: 14px;
51 | right: 14px;
52 | top: 8px;
53 | width: 14px;
54 | }
55 | .list-view-detail-content {
56 | padding: 10px;
57 | height: calc(100% - 40px);
58 | overflow: auto;
59 | overflow-x: hidden;
60 |
61 | .image-preview {
62 | height: 216px;
63 | display: flex;
64 | align-items: center;
65 | justify-content: center;
66 | img {
67 | max-height: 200px;
68 | max-width: 320px;
69 | min-width: 80px;
70 | min-height: 80px;
71 | }
72 | }
73 |
74 | .list-view-detail-content-item {
75 | margin-bottom: 5px;
76 | .list-view-detail-content-item-label {
77 | color: rgba(0, 0, 0, 0.6);
78 | font-size: 12px;
79 | }
80 | .list-view-detail-content-item-value {
81 | font-size: 14px;
82 | color: rgba(0, 0, 0, 1);
83 | word-break: break-all;
84 | &.head-request-info {
85 | color: rgba(0, 0, 0, 0.8);
86 | padding-left: 5px;
87 | font-size: 12px;
88 | line-height: 20px;
89 | }
90 | }
91 | }
92 | }
93 | }
94 | }
95 |
96 | .separate-line {
97 | border-top: 1px solid rgba(0, 0, 0, 0.12);
98 | }
99 |
100 | .list-operation {
101 | height: 40px;
102 | display: flex;
103 | border-bottom: 1px solid rgba(0, 0, 0, 0.12);
104 | align-items: center;
105 | // padding-left: 4px;
106 | .list-operation-item {
107 | color: rgba(0, 0, 0, 0.88);
108 | min-width: 22px;
109 | font-size: 14px;
110 | line-height: 24px;
111 | padding: 8px;
112 | margin-right: 8px;
113 | &:hover:not(.disabled) {
114 | cursor: pointer;
115 | background-color: #edf4fc;
116 | }
117 | &.list-operation-item-hover {
118 | cursor: pointer;
119 | background-color: #edf4fc;
120 | }
121 | &.disabled {
122 | color: rgba(0, 0, 0, 0.2);
123 | .svg-icon {
124 | opacity: 0.2;
125 | }
126 | }
127 | .svg-icon {
128 | margin-right: 3px;
129 | color: $upyunPrimaryColor;
130 | }
131 | }
132 | }
133 |
134 | .list {
135 | display: flex;
136 | flex: 1;
137 | overflow-y: scroll;
138 | overflow-x: hidden;
139 | }
140 |
141 | .empty-list {
142 | display: flex;
143 | align-items: center;
144 | flex: 1;
145 | .empty-list-content {
146 | text-align: center;
147 | flex: 1;
148 | font-size: 22px;
149 | }
150 | }
151 |
152 | .files-list {
153 | cursor: default;
154 |
155 | display: table;
156 | table-layout: fixed;
157 | padding-top: 25px;
158 | height: 100%;
159 | width: 100%;
160 |
161 | .files-list-header {
162 | background: #FAFAFA;
163 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08);
164 | position: absolute;
165 | color: #4c607a;
166 | font-size: 14px;
167 | display: flex;
168 | top: 40px;
169 | width: calc(100% - 16px);
170 | .column-file-name {
171 | flex: 1;
172 | }
173 | .file-info-item,.file-info-header {
174 | padding-top: 0;
175 | padding-bottom: 0;
176 | line-height: 25px;
177 | padding-left: 6px;
178 | &:first-child {
179 | padding-left: 12px;
180 | }
181 | &:not(:last-child) {
182 | border-right: 1px solid rgba(0, 0, 0, 0.08);
183 | }
184 | }
185 |
186 | .file-info-header {
187 | &:hover {
188 | color: rgba(0, 0, 0, 1);
189 | }
190 | .svg-icon {
191 | display: none;
192 | &.is-active {
193 | display: inline;
194 | }
195 | }
196 | }
197 | }
198 |
199 | .files-list-body {
200 | color: rgba(0, 0, 0, 1);
201 | display: table-row-group;
202 | }
203 |
204 | .files-list-column {
205 | display: table-column-group;
206 | }
207 |
208 | .table-column {
209 | display: table-column;
210 | }
211 |
212 | .column-file-name {
213 | width: 100%;
214 | }
215 | .column-last-modified {
216 | width: 170px;
217 | }
218 | .column-file-type {
219 | width: 150px;
220 | }
221 | .column-file-size {
222 | width: 120px;
223 | }
224 |
225 | .files-list-item {
226 | display: table-row;
227 | outline: none;
228 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.04);
229 | &:hover {
230 | background: #edf4fc;
231 | }
232 | &.item-selected {
233 | background: #cce8ff;
234 | }
235 | }
236 |
237 | .file-info-item {
238 | padding: 5px 6px 5px 10px;
239 | line-height: 24px;
240 | display: table-cell;
241 | &:first-child {
242 | padding-left: 12px;
243 | }
244 | }
245 |
246 | .mime,
247 | .last-modified,
248 | .size {
249 | color: rgba(0, 0, 0, 0.6);
250 | white-space: nowrap;
251 | overflow: hidden;
252 | text-overflow: ellipsis;
253 | }
254 |
255 | .name {
256 | overflow: hidden;
257 | text-overflow: ellipsis;
258 | white-space: nowrap;
259 | }
260 |
261 | .size {
262 | padding-right: 30px;
263 | text-align: right;
264 | }
265 | }
266 |
267 | .item-hover {
268 | background: #e5f3ff;
269 | }
270 |
271 | .empty-list-table {
272 | width: 100%;
273 | .empty-list-row {
274 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08);
275 | display: flex;
276 | min-height: 56px;
277 | }
278 | .empty-content {
279 | flex: 1;
280 | display: flex;
281 | flex-direction: column;
282 | justify-content: center;
283 | text-align: center;
284 | }
285 | }
286 |
--------------------------------------------------------------------------------
/src/renderer/styles/modal.scss:
--------------------------------------------------------------------------------
1 | .basic-modal.modal {
2 | .modal-content {
3 | padding: 24px 24px;
4 | background-color: white;
5 | box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1);
6 | color: rgba(0, 0, 0, 0.8);
7 | max-width: 100%;
8 | position: relative;
9 | }
10 | .modal-header {
11 | position: relative;
12 | line-height: 24px;
13 | margin-bottom: 24px;
14 | .modal-title {
15 | font-size: 20px;
16 | vertical-align: top;
17 | }
18 | .modal-close-button {
19 | outline: none;
20 | height: 14px;
21 | width: 14px;
22 | font-size: 14px;
23 | opacity: 0.7;
24 | position: absolute;
25 | right: 5px;
26 | top: 5px;
27 | cursor: pointer;
28 | .svg-icon {
29 | vertical-align: top;
30 | }
31 | }
32 | }
33 | .modal-footer {
34 | display: flex;
35 | justify-content: flex-end;
36 | margin-top: 24px;
37 | }
38 | }
39 |
40 | .modal-sm.modal {
41 | .modal-content {
42 | width: 320px;
43 | }
44 | }
45 |
46 | .modal-md.modal {
47 | .modal-content {
48 | width: 480px;
49 | }
50 | }
51 |
52 | .fade-enter-active,
53 | .fade-leave-active {
54 | transition: opacity 0.2s;
55 | }
56 | .fade-enter,
57 | .fade-leave-active {
58 | opacity: 0;
59 | }
60 |
61 | .modal-full .modal-content {
62 | height: 100%;
63 | max-height: 100vh;
64 | width: 100%;
65 | max-width: 100vw;
66 | }
67 |
--------------------------------------------------------------------------------
/src/renderer/styles/scroll.scss:
--------------------------------------------------------------------------------
1 | ::-webkit-scrollbar {
2 | height: 16px;
3 | overflow: visible;
4 | width: 16px
5 | }
6 |
7 | ::-webkit-scrollbar-track {
8 | background-clip: padding-box;
9 | border: solid transparent;
10 | border-width: 0 0 0 4px
11 | }
12 |
13 | ::-webkit-scrollbar-track:enabled {
14 | background-color: rgba(0, 0, 0, .05);
15 | }
16 |
17 | ::-webkit-scrollbar-track:hover {
18 | background-color: rgba(0, 0, 0, .05);
19 | box-shadow: inset 1px 0 0 rgba(0, 0, 0, .1)
20 | }
21 |
22 | ::-webkit-scrollbar-track:active {
23 | background-color: rgba(0, 0, 0, .05);
24 | box-shadow: inset 1px 0 0 rgba(0, 0, 0, .14), inset -1px 0 0 rgba(0, 0, 0, .07)
25 | }
26 |
27 | ::-webkit-scrollbar-thumb {
28 | background-color: rgba(0, 0, 0, .2);
29 | background-clip: padding-box;
30 | border: solid transparent;
31 | border-width: 1px 1px 1px 6px;
32 | min-height: 28px;
33 | padding: 100px 0 0;
34 | box-shadow: inset 1px 1px 0 rgba(0, 0, 0, .1), inset 0 -1px 0 rgba(0, 0, 0, .07)
35 | }
36 |
37 | ::-webkit-scrollbar-thumb:hover {
38 | background-color: rgba(0, 0, 0, .4);
39 | box-shadow: inset 1px 1px 1px rgba(0, 0, 0, .25)
40 | }
41 |
42 | ::-webkit-scrollbar-thumb:active {
43 | background-color: rgba(0, 0, 0, 0.5);
44 | box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.35)
45 | }
--------------------------------------------------------------------------------
/src/renderer/styles/task.scss:
--------------------------------------------------------------------------------
1 | .task-container {
2 | width: 100%;
3 | background: #ffffff;
4 | position: relative;
5 | display: flex;
6 | flex-direction: column;
7 | .tabs {
8 | border-bottom: 1px solid rgba(0, 0, 0, 0.12);
9 | }
10 | .tabs a {
11 | height: 40px;
12 | }
13 | .tabs .handle a {
14 | &:hover {
15 | // 和正常时一样
16 | border-bottom-color: #dbdbdb;
17 | }
18 | }
19 | .tabs:not(:last-child) {
20 | margin-bottom: 0;
21 | }
22 | .files-list {
23 | .column-file-name {
24 | width: 100%;
25 | }
26 | .column-file-size {
27 | width: 150px;
28 | }
29 | .column-file-status {
30 | width: 250px;
31 | }
32 | .column-file-handle {
33 | width: 150px;
34 | }
35 | }
36 | .files-list-item {
37 | height: 56px;
38 | .file-info-item, .file-info-header {
39 | vertical-align: middle;
40 | }
41 | }
42 | .name.file-info-item {
43 | a {
44 | color: rgb(0, 0, 0);
45 | &:hover {
46 | color: rgba(0, 0, 0, 0.6);
47 | }
48 | }
49 | }
50 | .size.file-info-item {
51 | font-size: 12px;
52 | text-align: left;
53 | padding-right: 15px;
54 | }
55 | .status.file-info-item {
56 | font-size: 12px;
57 | text-align: left;
58 | padding-right: 15px;
59 | color: rgba(0, 0, 0, 0.8);
60 | line-height: 18px;
61 | .task-state {
62 | display: flex;
63 | }
64 | .task-state-time {
65 | margin-left: 16px;
66 | color: rgba(0, 0, 0, 0.4);
67 | }
68 | }
69 | .handle.file-info-item {
70 | line-height: 18px;
71 | }
72 | }
73 |
74 | .file-info-item {
75 | .img-mini-preview {
76 | width: 24px;
77 | height: 24px;
78 | object-fit: cover;
79 | vertical-align: -7px;
80 | }
81 | }
82 |
83 |
--------------------------------------------------------------------------------
/src/renderer/views/download/Download.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
16 |
22 |
23 |
28 |
29 | {{file.filename}}
30 |
31 |
32 | {{file.transferred | digiUnit}} / {{file.total | digiUnit}}
33 |
34 |
35 |
36 | {{file.errorMessage}}
37 |
38 |
39 |
40 |
41 | {{file.status && task.status[file.status].name}}
42 |
43 |
44 | {{file.endTime | timestamp('YYYY-MM-DD')}}
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | {{file.status && task.status[file.status].name}} {{getProgress(file) | percent}}
53 |
54 |
55 | {{file.startTime | timestamp('YYYY-MM-DD')}}
56 |
57 |
58 |
59 |
60 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
73 |
74 |
75 |
76 |
77 |
84 |
85 |
86 |
87 |
163 |
--------------------------------------------------------------------------------
/src/renderer/views/layout/LayoutBody.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/renderer/views/layout/LayoutMenu.vue:
--------------------------------------------------------------------------------
1 |
2 |
109 |
110 |
111 |
201 |
--------------------------------------------------------------------------------
/src/renderer/views/layout/LayoutNav.vue:
--------------------------------------------------------------------------------
1 |
2 |
44 |
45 |
46 |
118 |
--------------------------------------------------------------------------------
/src/renderer/views/layout/Main.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
56 |
--------------------------------------------------------------------------------
/src/renderer/views/list/List.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 获取链接
7 |
8 |
9 | 下载
10 |
11 |
12 | 查看
13 |
14 |
15 | 删除
16 |
17 |
18 | 重命名
19 |
20 |
25 | 详情
26 |
27 |
28 |
38 |
42 |
48 |
70 |
71 |
81 |
82 | {{file.filename}}
83 |
84 |
{{file.lastModified | timestamp}}
85 |
{{file.filetype}}
86 |
{{(file.folderType === 'F' ? '-' : file.size) | digiUnit}}
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
该文件夹为空
95 |
拖动到此处即可上传文件,或点击上传
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
118 |
119 |
120 |
121 |
122 |
![]()
123 |
124 |
125 |
126 | 添加日期
127 |
128 |
129 | {{fileDetail.basicInfo.lastModified | timestamp}}
130 |
131 |
132 |
133 |
134 | 大小
135 |
136 |
137 | {{(fileDetail.basicInfo.folderType === 'F' ? '-' : fileDetail.basicInfo.size) | digiUnit}}
138 |
139 |
140 |
141 |
142 | 链接
143 |
144 |
161 |
162 |
163 |
164 | Response Headers
165 |
166 |
167 |
168 | {{key}} → {{value}}
169 |
170 |
171 |
172 |
173 |
174 |
175 |
181 | {{`确定要删除「${getBasename(selected[0])}」${selected.length > 1 ? `等${selected.length}个文件` : ''}吗?`}}
182 | 该操作无法恢复。
183 |
184 |
185 |
186 |
187 |
575 |
--------------------------------------------------------------------------------
/src/renderer/views/login/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | 帐号历史
11 |
18 |
19 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
127 |
--------------------------------------------------------------------------------
/src/renderer/views/modal/CreateFolder.vue:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
23 |
80 |
--------------------------------------------------------------------------------
/src/renderer/views/modal/DomainSetting.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
13 |
21 |
22 |
请输入包含 http:// 或 https:// 的正确的域名
23 |
24 |
25 |
26 |
获取链接之前,需要指定一个加速域名用来生成链接(包含 http:// 或 https://)。你可以通过访问又拍云控制台查看你绑定的加速域名。
27 |
在 2017-10-26 前创建的服务绑定的默认的加速域名的格式:http://{yourBucketName}.b0.upaiyun.com
28 |
新创建的服务绑定的默认的加速域名的格式:http://{yourBucketName}.test.upcdn.net
29 |
30 |
31 |
32 |
35 |
36 |
37 |
38 |
39 |
111 |
--------------------------------------------------------------------------------
/src/renderer/views/modal/FileProgress.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
14 |
15 | {{data.filename}}
16 |
17 |
18 | {{data.done | digiUnit}}/{{data.size | digiUnit}} {{data.status | uploadStatus}}
19 |
20 |
21 |
24 |
25 |
26 |
27 |
28 |
29 |
74 |
--------------------------------------------------------------------------------
/src/renderer/views/modal/FormatUrl.vue:
--------------------------------------------------------------------------------
1 |
2 |
35 |
36 |
37 |
128 |
--------------------------------------------------------------------------------
/src/renderer/views/modal/RenameFile.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
请输入新的文件路径:
13 |
23 |
24 |
27 |
28 |
29 |
30 |
31 |
89 |
--------------------------------------------------------------------------------
/src/renderer/views/modal/UploadHandle.vue:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/renderer/views/upload/Upload.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
16 |
22 |
23 |
28 |
29 | {{file.filename}}
30 |
31 |
32 | {{file.transferred | digiUnit}} / {{file.total | digiUnit}}
33 |
34 |
35 |
36 | {{file.errorMessage}}
37 |
38 |
39 |
40 |
41 | {{file.status && task.status[file.status].name}}
42 |
43 |
44 | {{file.endTime | timestamp('YYYY-MM-DD')}}
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | {{file.status && task.status[file.status].name}} {{getProgress(file) | percent}}
53 |
54 |
55 | {{file.startTime | timestamp('YYYY-MM-DD')}}
56 |
57 |
58 |
59 |
60 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
73 |
74 |
75 |
76 |
77 |
84 |
85 |
86 |
87 |
174 |
--------------------------------------------------------------------------------
/static/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ntduan/updrive/8000819a68671e52e1ad802e0b50a686e5a3817e/static/.gitkeep
--------------------------------------------------------------------------------
/static/screenshot1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ntduan/updrive/8000819a68671e52e1ad802e0b50a686e5a3817e/static/screenshot1.png
--------------------------------------------------------------------------------
/static/screenshot2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ntduan/updrive/8000819a68671e52e1ad802e0b50a686e5a3817e/static/screenshot2.png
--------------------------------------------------------------------------------
/static/screenshot3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ntduan/updrive/8000819a68671e52e1ad802e0b50a686e5a3817e/static/screenshot3.png
--------------------------------------------------------------------------------