├── .angular-cli.json ├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── help.md ├── package.json ├── src ├── main │ ├── api │ │ ├── api.ts │ │ ├── file-api.ts │ │ ├── index.ts │ │ └── setting-api.ts │ ├── configs.ts │ ├── main.ts │ ├── service │ │ ├── index.ts │ │ ├── menu.ts │ │ └── qiniu.ts │ ├── tsconfig.main.json │ └── utils.ts └── renderer │ ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.ts │ ├── app.module.ts │ ├── components │ │ ├── components.module.ts │ │ ├── file-icon │ │ │ ├── file-icon.component.html │ │ │ ├── file-icon.component.scss │ │ │ └── file-icon.component.ts │ │ └── progress │ │ │ ├── progress.component.html │ │ │ ├── progress.component.scss │ │ │ └── progress.component.ts │ ├── nav │ │ ├── nav.component.html │ │ ├── nav.component.scss │ │ └── nav.component.ts │ ├── service │ │ ├── base.service.ts │ │ ├── file.service.ts │ │ ├── router.service.ts │ │ └── setting.service.ts │ ├── setting │ │ ├── setting.component.html │ │ ├── setting.component.scss │ │ ├── setting.component.ts │ │ └── setting.model.ts │ └── upload │ │ ├── upload-details │ │ ├── upload-details.component.html │ │ ├── upload-details.component.scss │ │ └── upload-details.component.ts │ │ ├── upload.component.html │ │ ├── upload.component.scss │ │ ├── upload.component.ts │ │ └── upload.module.ts │ ├── assets │ ├── iconfont │ │ ├── iconfont.css │ │ ├── iconfont.eot │ │ ├── iconfont.svg │ │ ├── iconfont.ttf │ │ └── iconfont.woff │ ├── mixin.scss │ └── reset.css │ ├── environments │ ├── environment.prod.ts │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles.scss │ ├── tsconfig.app.json │ └── typings.d.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "project": { 4 | "name": "drag-upload-qiniu" 5 | }, 6 | "apps": [{ 7 | "root": "src/renderer", 8 | "outDir": "out/dist", 9 | "assets": [ 10 | "assets", 11 | "favicon.ico" 12 | ], 13 | "index": "index.html", 14 | "main": "main.ts", 15 | "polyfills": "polyfills.ts", 16 | "tsconfig": "tsconfig.app.json", 17 | "prefix": "app", 18 | "styles": [ 19 | "styles.scss" 20 | ], 21 | "scripts": [], 22 | "environmentSource": "environments/environment.ts", 23 | "environments": { 24 | "dev": "environments/environment.ts", 25 | "prod": "environments/environment.prod.ts" 26 | } 27 | }], 28 | "defaults": { 29 | "styleExt": "scss", 30 | "component": {} 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | .vscode 20 | 21 | # IDE - VSCode 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | 28 | # misc 29 | /.sass-cache 30 | /connect.lock 31 | /coverage 32 | /libpeerconnection.log 33 | npm-debug.log 34 | testem.log 35 | /typings 36 | yarn-error.log 37 | 38 | # e2e 39 | /e2e/*.js 40 | /e2e/*.map 41 | 42 | # System Files 43 | .DS_Store 44 | Thumbs.db 45 | 46 | package-lock.json 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 lleohao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Qiniu-upload 2 | 3 | > 基于Electron和七牛的图片上传工具 4 | > 5 | > GIF预览效果不好, 可以查看[视频预览](http://ofwqfk202.bkt.clouddn.com/qiniu-upload.webm) 6 | 7 | ![预览](http://ofwqfk202.bkt.clouddn.com/qiniu-upload.gif) 8 | 9 | [下载地址](https://github.com/lleohao/qiniu-upload/releases/download/v1.0.1/Upload-1.0.1.dmg) [国内下载地址](http://ofwqfk202.bkt.clouddn.com/Upload-1.0.1-mac.zip) 10 | 11 | 12 | 13 | ### 软件界面 14 | 15 | 1. 未进行任何设置时会自动跳转至设置界面 16 | 17 | ![设置界面](http://ofwqfk202.bkt.clouddn.com/qiniu-upload-setting.png) 18 | 19 | > AccessKey/SecretKey 在七牛个人中心/密钥管理 20 | > 21 | > 空间名称/空间域名 在对应的存储空间列表中, 域名设置时需要带上对应的协议(http:// 或 https://) 22 | > 23 | > **设置完成后将鼠标放置在软件顶部会出现返回图标, 点击后返回上传界面** 24 | 25 | 2. 上传界面 26 | 27 | ![上传界面](http://ofwqfk202.bkt.clouddn.com/qiniu-upload-main.png) 28 | 29 | > 可以拖拽文件上传或者点击 `browse` 进行文件选择 30 | 31 | 3. 上传结果页面 32 | 33 | ![结果页面](http://ofwqfk202.bkt.clouddn.com/qiniu-upload-result.png) 34 | 35 | > 上传过程中会有进度显示, 完成后进度条消失 36 | > 37 | > **点击文件名可以获取文件的外链地址** 38 | 39 | 40 | 41 | ### 如何参与开发 42 | 43 | 1. bug可以在Issues中提出 44 | 2. 功能需求也可以在Issues中提出 45 | 3. 自行开发流程 46 | 1. fork 本项目, clone至本地 47 | 2. 运行`yarn instll`安装依赖 48 | 3. 运行`npm run watch:main`编译主进程相关代码 49 | 4. 运行`npm run watch:renderer`编译渲染进程代码 50 | 5. 运行`npm start`启动软件 51 | 6. 开发完成后发送pr给我, **不要在master分支提交pr** 52 | 53 | 54 | 55 | ### 其他 56 | 57 | 觉得好用的话可否请开发者加个🍗或者来杯☕️ 58 | 59 | ![money](http://ofwqfk202.bkt.clouddn.com/money.png) 60 | 61 | -------------------------------------------------------------------------------- /help.md: -------------------------------------------------------------------------------- 1 | # Upload F&Q 2 | 3 | **F: 设置完成后如何返回主界面?** 4 | 5 | > 鼠标放在软件顶部会出现返回按钮 6 | 7 | **F: 设置页面的值在哪里可以找到?** 8 | 9 | > 1. AccessKey/SecretKey 在七牛个人中心/密钥管理 10 | > 2. 空间名称/空间域名 在对应的存储空间列表中, 域名设置时需要带上对应的协议(http:// 或 https://) 11 | 12 | **F: 上传进度一直没动是怎么回事?** 13 | 14 | > 应该是上传错误了, 但是我忘记做错误处理😶 15 | > 16 | > 下次加上 17 | 18 | **F: 还有其他问题?** 19 | 20 | > 可以提一个issure, 有时间的话会处理 21 | 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Upload", 3 | "version": "1.0.1", 4 | "license": "MIT", 5 | "author": "lleohao ", 6 | "description": "A file upload app for qiniu", 7 | "scripts": { 8 | "ng": "ng", 9 | "start": "nodemon --watch src/main --watch src/shared --exec './node_modules/.bin/electron' ./out/main/main.js", 10 | "dev:renderer": "ng serve", 11 | "dev:main": "tsc -p ./src/main/tsconfig.main.json -w", 12 | "pack": "electron-builder -m --dir", 13 | "build:renderer": "ng build --prod", 14 | "build:main": "tsc -p ./src/main/tsconfig.main.json", 15 | "dist": "electron-builder -m", 16 | "build": "npm run build:renderer && npm run build:main && npm run dist " 17 | }, 18 | "main": "out/main.js", 19 | "private": true, 20 | "dependencies": { 21 | "@angular/animations": "^4.4.6", 22 | "@angular/common": "^4.4.6", 23 | "@angular/compiler": "^4.4.6", 24 | "@angular/core": "^4.4.6", 25 | "@angular/forms": "^4.4.6", 26 | "@angular/http": "^4.4.6", 27 | "@angular/platform-browser": "^4.4.6", 28 | "@angular/platform-browser-dynamic": "^4.4.6", 29 | "@angular/platform-server": "^4.4.6", 30 | "@angular/router": "^4.4.6", 31 | "core-js": "^2.5.3", 32 | "electron-settings": "^3.1.4", 33 | "qiniu": "^7.1.2", 34 | "rxjs": "^5.5.6", 35 | "zone.js": "^0.8.20" 36 | }, 37 | "devDependencies": { 38 | "@angular/cli": "1.3.1", 39 | "@angular/compiler-cli": "^4.4.6", 40 | "@angular/language-service": "^4.4.6", 41 | "@types/node": "^6.0.96", 42 | "codelyzer": "~3.1.1", 43 | "electron": "^1.8.8", 44 | "electron-builder": "^19.55.3", 45 | "mockjs": "^1.0.1-beta3", 46 | "nodemon": "^1.14.11", 47 | "ts-node": "~3.2.0", 48 | "tslint": "~5.3.2", 49 | "typescript": "~2.3.3" 50 | }, 51 | "build": { 52 | "appId": "com.lleohao.dragUpload", 53 | "mac": { 54 | "category": "public.app-category.productivity" 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/api/api.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron'; 2 | 3 | class Api { 4 | private path: Set; 5 | 6 | constructor() { 7 | this.path = new Set(); 8 | } 9 | 10 | public add(channel: string, listener: (e: Electron.Event, ...args) => void) { 11 | if (this.path.has(channel)) { 12 | throw new Error(`path: ${channel} is exites in app.`); 13 | } 14 | 15 | this.path.add(channel); 16 | ipcMain.on(channel, listener); 17 | } 18 | } 19 | 20 | export default new Api(); 21 | -------------------------------------------------------------------------------- /src/main/api/file-api.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs'; 3 | import { app, dialog, ipcMain } from 'electron'; 4 | 5 | import api from './api'; 6 | import configs from '../configs'; 7 | import { Upload, UploadFile } from '../service/qiniu'; 8 | 9 | /** 10 | * 获取上传对象 11 | */ 12 | const getClient = (function () { 13 | let client = null; 14 | 15 | return (): Upload => { 16 | if (client === null || configs.reflush) { 17 | client = new Upload(configs.setting); 18 | configs.reflush = false; 19 | } 20 | return client; 21 | }; 22 | })(); 23 | 24 | /** 25 | * 上传成功处理函数 26 | * 27 | * @param err 七牛上传错误对象 28 | * @param resBody 七牛上传响应体 29 | * @param code 七牛上传 HTTP code 30 | * @param id 文件唯一id 31 | */ 32 | const resCb = (e: Electron.Event) => { 33 | return (id: string, err, body, code) => { 34 | if (err !== null) { 35 | dialog.showErrorBox('上传失败', err); 36 | e.sender.send('/file/upload/error', { id, error: err }); 37 | return; 38 | } 39 | 40 | if (code !== undefined) { 41 | const errorMessage = `http code: ${code}, ${body}`; 42 | dialog.showErrorBox('上传失败', errorMessage); 43 | e.sender.send('/file/upload/error', { id, error: errorMessage }); 44 | return; 45 | } 46 | }; 47 | }; 48 | 49 | /** 50 | * 上传进度处理函数 51 | * 52 | * @param id 文件唯一id 53 | * @param progress 上传进度 54 | */ 55 | const progressCb = (e: Electron.Event) => { 56 | return (id: string, progress: number) => { 57 | e.sender.send('/file/upload/progress', { 58 | id, 59 | progress 60 | }); 61 | }; 62 | }; 63 | 64 | /** 65 | * 上传文件函数 66 | * 67 | * @param e Electron event 68 | * @param filePaths 上传文件的路径 69 | */ 70 | const uploadFile = (e: Electron.Event, filePaths: string[]) => { 71 | const uploadClient = getClient(); 72 | const files = filePaths.map((filePath) => { 73 | const parsed = path.parse(filePath); 74 | const size = fs.statSync(filePath).size; 75 | return { 76 | baseName: parsed.base, 77 | fileName: parsed.name, 78 | localPath: filePath, 79 | size: size, 80 | ext: parsed.ext.substr(1) 81 | }; 82 | }); 83 | 84 | e.sender.send('/file/uploadlist', files); 85 | 86 | process.nextTick(() => { 87 | files.forEach(({ baseName, localPath, size }) => { 88 | uploadClient.uploadFile({ 89 | fileName: baseName, 90 | localPath, 91 | size, 92 | progressCb: progressCb(e), 93 | resCb: resCb(e) 94 | }); 95 | }); 96 | }); 97 | }; 98 | 99 | /** 100 | * 打开文件选择框 101 | */ 102 | api.add('/file/select', (e) => { 103 | dialog.showOpenDialog({ 104 | title: 'Select file', 105 | message: 'select file', 106 | properties: ['multiSelections', 'openFile'] 107 | }, (filePaths: string[]) => { 108 | uploadFile(e, filePaths); 109 | }); 110 | }); 111 | 112 | /** 113 | * 上传文件 114 | */ 115 | api.add('/file/drop', (e, filePaths: string[]) => { 116 | uploadFile(e, filePaths); 117 | }); 118 | -------------------------------------------------------------------------------- /src/main/api/index.ts: -------------------------------------------------------------------------------- 1 | import './setting-api'; 2 | import './file-api'; 3 | -------------------------------------------------------------------------------- /src/main/api/setting-api.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'electron'; 2 | import configs from '../configs'; 3 | 4 | import api from './api'; 5 | 6 | export interface Settings { 7 | ak: string; 8 | sk: string; 9 | scope: string; 10 | domain: string; 11 | } 12 | 13 | api.add('/setting/save', (e, settings) => { 14 | configs.save(settings); 15 | }); 16 | 17 | api.add('/setting/clear', (e) => { 18 | configs.clear(); 19 | }); 20 | 21 | api.add('/setting/get', (e) => { 22 | e.returnValue = configs.setting; 23 | }); 24 | -------------------------------------------------------------------------------- /src/main/configs.ts: -------------------------------------------------------------------------------- 1 | import * as electronSetting from 'electron-settings'; 2 | 3 | export interface QiNiuSetting { 4 | ak: string; 5 | sk: string; 6 | scope: string; 7 | domain: string; 8 | } 9 | 10 | class Config { 11 | setting: QiNiuSetting = { 12 | ak: '', 13 | sk: '', 14 | scope: '', 15 | domain: '' 16 | }; 17 | reflush = false; 18 | 19 | constructor() { } 20 | 21 | init() { 22 | if (electronSetting.has('certificate')) { 23 | this.setting = electronSetting.get('certificate'); 24 | } 25 | } 26 | 27 | save(settings) { 28 | this.reflush = true; 29 | this.setting = settings; 30 | electronSetting.set('certificate', settings); 31 | } 32 | 33 | clear() { 34 | this.setting = { 35 | ak: '', 36 | sk: '', 37 | scope: '', 38 | domain: '' 39 | }; 40 | electronSetting.deleteAll(); 41 | } 42 | } 43 | 44 | export default new Config(); 45 | -------------------------------------------------------------------------------- /src/main/main.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, ipcMain, Menu, shell } from 'electron'; 2 | 3 | import * as path from 'path'; 4 | import configs from './configs'; 5 | 6 | import { CustomMenu } from './service'; 7 | import { isDev } from './utils'; 8 | 9 | // Init project 10 | import './api/'; // 导入api 11 | 12 | let mainWin: Electron.BrowserWindow; 13 | 14 | function createWindow() { 15 | mainWin = new BrowserWindow({ 16 | width: 680, 17 | height: 500, 18 | minHeight: 500, 19 | minWidth: 680, 20 | title: '七牛上传工具' 21 | }); 22 | const webContents = mainWin.webContents; 23 | 24 | // open windows 25 | if (isDev()) { 26 | mainWin.loadURL('http://127.0.0.1:4200'); 27 | mainWin.webContents.openDevTools(); 28 | } else { 29 | mainWin.loadURL(`file://${path.resolve(__dirname, 'dist/index.html')}`); 30 | } 31 | 32 | CustomMenu(); 33 | configs.init(); 34 | 35 | mainWin.on('closed', () => { 36 | mainWin = null; 37 | }); 38 | } 39 | 40 | app.on('ready', createWindow); 41 | 42 | app.on('window-all-closed', () => { 43 | if (process.platform !== 'darwin') { 44 | app.quit(); 45 | } 46 | }); 47 | 48 | app.on('activate', () => { 49 | if (mainWin === null) { 50 | createWindow(); 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /src/main/service/index.ts: -------------------------------------------------------------------------------- 1 | export { CustomMenu } from './menu'; 2 | -------------------------------------------------------------------------------- /src/main/service/menu.ts: -------------------------------------------------------------------------------- 1 | import { app, Menu, shell, webContents } from 'electron'; 2 | 3 | export const CustomMenu = () => { 4 | const template = [{ 5 | label: 'Application', 6 | submenu: [ 7 | { label: 'About Application', selector: 'orderFrontStandardAboutPanel:' }, 8 | { 9 | label: 'Preferences...', accelerator: 'Command+,', click: function () { 10 | webContents.getFocusedWebContents().send('/open/setting'); 11 | } 12 | }, 13 | { type: 'separator' }, 14 | { label: 'Quit', accelerator: 'Command+Q', click: function () { app.quit(); } } 15 | ] 16 | }, { 17 | label: 'Edit', 18 | submenu: [ 19 | { label: 'Undo', accelerator: 'CmdOrCtrl+Z', selector: 'undo:' }, 20 | { label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', selector: 'redo:' }, 21 | { type: 'separator' }, 22 | { label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:' }, 23 | { label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:' }, 24 | { label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:' }, 25 | { label: 'Select All', accelerator: 'CmdOrCtrl+A', selector: 'selectAll:' } 26 | ] 27 | }, 28 | { 29 | label: 'Help', 30 | submenu: [ 31 | { 32 | label: 'Document', click: function () { 33 | shell.openExternal('https://github.com/lleohao/dragUpload-qiniu/blob/master/help.md'); 34 | } 35 | } 36 | ] 37 | }]; 38 | Menu.setApplicationMenu(Menu.buildFromTemplate(template as Electron.MenuItemConstructorOptions[])); 39 | }; 40 | -------------------------------------------------------------------------------- /src/main/service/qiniu.ts: -------------------------------------------------------------------------------- 1 | import * as qiniu from 'qiniu'; 2 | import * as fs from 'fs'; 3 | 4 | const MAX_UPLOAD_COUNT = 5; 5 | const TOKEN_EXPIRES = 3600; 6 | 7 | export interface UploadFile { 8 | localPath: string; 9 | fileName: string; 10 | progressCb: (id, progress) => void; 11 | resCb: (id, err, body?, code?) => void; 12 | id?: number | string; 13 | size?: number; 14 | } 15 | 16 | export class Upload { 17 | private putPolicy; 18 | private mac; 19 | private config; 20 | private token; 21 | private tokenValidPeriod; 22 | private expires = TOKEN_EXPIRES; 23 | 24 | private MAX_UPLOAD_COUNT = 5; 25 | private uploadQueue: UploadFile[] = []; 26 | private inUpload = 0; 27 | 28 | constructor({ ak, sk, scope }) { 29 | this.mac = new qiniu.auth.digest.Mac(ak, sk); 30 | 31 | this.putPolicy = new qiniu.rs.PutPolicy({ 32 | scope: scope, 33 | expires: this.expires 34 | }); 35 | 36 | this.config = new qiniu.conf.Config(); 37 | this.config.zone = qiniu.zone.Zone_z2; 38 | } 39 | 40 | private getUploadToken() { 41 | const now = Date.now(); 42 | 43 | if (!this.tokenValidPeriod || (now + this.expires * 1000) < this.tokenValidPeriod) { 44 | this.token = this.putPolicy.uploadToken(this.mac); 45 | } 46 | 47 | return this.token; 48 | } 49 | 50 | public uploadFile({ localPath, fileName, progressCb, resCb, id, size }: UploadFile) { 51 | const uploadToken = this.getUploadToken(); 52 | id = id || localPath; 53 | 54 | const resumeUploader = new qiniu.resume_up.ResumeUploader(this.config); 55 | const putExtra = new qiniu.resume_up.PutExtra(null, {}, null, null, (uploadSize) => { 56 | progressCb(id, Math.floor(uploadSize / size * 100)); 57 | }); 58 | 59 | if (this.inUpload <= this.MAX_UPLOAD_COUNT) { 60 | this.inUpload++; 61 | 62 | resumeUploader.putFile(uploadToken, fileName, localPath, putExtra, (err, body, respInfo) => { 63 | this.inUpload--; 64 | if (this.uploadQueue.length !== 0) { 65 | this.uploadFile(this.uploadQueue.pop()); 66 | } 67 | 68 | if (err) { 69 | resCb(id, err); 70 | } 71 | if (respInfo.statusCode === 200) { 72 | resCb(id, null, body); 73 | } else { 74 | resCb(id, null, body, respInfo.code); 75 | } 76 | }); 77 | } else { 78 | this.uploadQueue.push({ 79 | localPath, 80 | fileName, 81 | progressCb, 82 | resCb 83 | }); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/tsconfig.main.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es2015" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/utils.ts: -------------------------------------------------------------------------------- 1 | export const isDev = function () { 2 | return process.env.NODE_ENV && process.env.NODE_ENV === 'development'; 3 | }; 4 | -------------------------------------------------------------------------------- /src/renderer/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { SettingComponent } from './setting/setting.component'; 5 | import { UploadComponent } from './upload/upload.component'; 6 | 7 | import { RouterService } from './service/router.service'; 8 | 9 | const routes: Routes = [ 10 | { 11 | path: 'upload', 12 | canActivate: [RouterService], 13 | component: UploadComponent 14 | }, 15 | { 16 | path: 'setting', 17 | component: SettingComponent 18 | }, 19 | { 20 | path: '', 21 | redirectTo: '/upload', 22 | pathMatch: 'full' 23 | } 24 | ]; 25 | 26 | @NgModule({ 27 | imports: [RouterModule.forRoot(routes)], 28 | exports: [RouterModule] 29 | }) 30 | export class AppRoutingModule { } 31 | -------------------------------------------------------------------------------- /src/renderer/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/renderer/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lleohao/qiniu-upload/6073e8b28b577c0a1a58f70b5a523782417c3e7f/src/renderer/app/app.component.scss -------------------------------------------------------------------------------- /src/renderer/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | export class AppComponent { 9 | } 10 | -------------------------------------------------------------------------------- /src/renderer/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | 6 | import { AppRoutingModule } from './app-routing.module'; 7 | import { UploadModule } from './upload/upload.module'; 8 | 9 | import { AppComponent } from './app.component'; 10 | import { SettingComponent } from './setting/setting.component'; 11 | import { NavComponent } from './nav/nav.component'; 12 | 13 | import { FileService } from './service/file.service'; 14 | import { SettingService } from './service/setting.service'; 15 | import { RouterService } from './service/router.service'; 16 | 17 | @NgModule({ 18 | imports: [ 19 | BrowserModule, 20 | BrowserAnimationsModule, 21 | FormsModule, 22 | 23 | AppRoutingModule, 24 | UploadModule 25 | ], 26 | declarations: [ 27 | AppComponent, 28 | SettingComponent, 29 | NavComponent 30 | ], 31 | providers: [FileService, SettingService, RouterService], 32 | bootstrap: [AppComponent] 33 | }) 34 | export class AppModule { } 35 | -------------------------------------------------------------------------------- /src/renderer/app/components/components.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FileIconComponent } from './file-icon/file-icon.component'; 4 | import { ProgressComponent } from './progress/progress.component'; 5 | 6 | @NgModule({ 7 | imports: [CommonModule], 8 | exports: [ 9 | FileIconComponent, 10 | ProgressComponent 11 | ], 12 | declarations: [FileIconComponent, ProgressComponent] 13 | }) 14 | export class ComponentsModule { } 15 | -------------------------------------------------------------------------------- /src/renderer/app/components/file-icon/file-icon.component.html: -------------------------------------------------------------------------------- 1 |
2 | 8 | 10 | {{ext|uppercase}} 16 | 17 |
18 | -------------------------------------------------------------------------------- /src/renderer/app/components/file-icon/file-icon.component.scss: -------------------------------------------------------------------------------- 1 | .jpg, 2 | .jpeg, 3 | .png, 4 | .bmp, 5 | .gif { 6 | fill: #5B87E8; 7 | } 8 | 9 | .mp3, 10 | .wma, 11 | .wav, 12 | .ape, 13 | .flac, 14 | .ogg, 15 | .aac { 16 | fill: #57B079; 17 | } 18 | 19 | .mp4, 20 | .avi, 21 | .mov, 22 | .wmv, 23 | .mkv { 24 | fill: #EF8C42; 25 | } 26 | 27 | .zip, 28 | .rar, 29 | .gz, 30 | .iso { 31 | fill: #F0D242; 32 | } 33 | 34 | .bmg, 35 | .app, 36 | .exe { 37 | fill: #e75751; 38 | } 39 | 40 | .txt, 41 | .md, 42 | .js, 43 | .css, 44 | .html, 45 | .ts { 46 | fill: #00B7E5; 47 | } 48 | 49 | .file-icon { 50 | display: inline-block; 51 | vertical-align: -webkit-baseline-middle; 52 | } 53 | -------------------------------------------------------------------------------- /src/renderer/app/components/file-icon/file-icon.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-file-icon', 5 | templateUrl: './file-icon.component.html', 6 | styleUrls: ['./file-icon.component.scss'] 7 | }) 8 | export class FileIconComponent implements OnInit { 9 | @Input() ext: string; 10 | 11 | constructor() { } 12 | 13 | ngOnInit() { 14 | if (this.ext.startsWith('.')) { 15 | this.ext = this.ext.substr(1); 16 | } 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/renderer/app/components/progress/progress.component.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 11 | 19 | 20 |
21 | -------------------------------------------------------------------------------- /src/renderer/app/components/progress/progress.component.scss: -------------------------------------------------------------------------------- 1 | .progress { 2 | display: inline-block; 3 | vertical-align: -webkit-baseline-middle; 4 | .progress-circle { 5 | transition: .2s ease-in-out stroke-dasharray; 6 | transform: matrix(0,-1,1,0,0,24); 7 | 8 | stroke-dasharray: 0 73px; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/renderer/app/components/progress/progress.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-progress', 5 | templateUrl: './progress.component.html', 6 | styleUrls: ['./progress.component.scss'] 7 | }) 8 | export class ProgressComponent implements OnChanges { 9 | @Input() progress: number; 10 | dasharray: string; 11 | 12 | ngOnChanges() { 13 | const percent = this.progress / 100; 14 | const perimeter = Math.PI * 2 * 10; 15 | 16 | this.dasharray = perimeter * percent + ' ' + perimeter * (1 - percent); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/renderer/app/nav/nav.component.html: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /src/renderer/app/nav/nav.component.scss: -------------------------------------------------------------------------------- 1 | nav { 2 | padding: 0 16px; 3 | height: 48px; 4 | line-height: 48px; 5 | .nav-wrap { 6 | display: flex; 7 | justify-content: space-between; 8 | transform: translateY(-48px); 9 | opacity: 0; 10 | transition: all ease-in .3s; 11 | } 12 | &:hover .nav-wrap { 13 | opacity: 1; 14 | transform: translateY(0); 15 | } 16 | .iconfont { 17 | font-size: 18px; 18 | &.hidden { 19 | visibility: hidden; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/app/nav/nav.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Router, NavigationEnd } from '@angular/router'; 3 | 4 | import 'rxjs/add/operator/map'; 5 | 6 | @Component({ 7 | selector: 'app-nav', 8 | templateUrl: './nav.component.html', 9 | styleUrls: ['./nav.component.scss'] 10 | }) 11 | export class NavComponent { 12 | hidden = true; 13 | 14 | constructor(private router: Router) { 15 | this.router.events.subscribe((e) => { 16 | if (e instanceof NavigationEnd) { 17 | this.hidden = e.url !== '/setting'; 18 | } 19 | }); 20 | } 21 | 22 | goBack() { 23 | this.router.navigateByUrl('/upload'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/renderer/app/service/base.service.ts: -------------------------------------------------------------------------------- 1 | export class BaseService { 2 | protected ipcRender = electron.ipcRenderer; 3 | 4 | protected uuid() { 5 | return Math.random().toString(36).slice(2); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/renderer/app/service/file.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs/Observable'; 3 | import { Subscriber } from 'rxjs/Subscriber'; 4 | 5 | import { BaseService } from './base.service'; 6 | 7 | export interface SelectedFile { 8 | localPath: string; 9 | fileName: string; 10 | size: number; 11 | ext: string; 12 | } 13 | 14 | export interface ProgressInterface { 15 | id: string; 16 | progress: number; 17 | } 18 | 19 | @Injectable() 20 | export class FileService extends BaseService { 21 | constructor() { 22 | super(); 23 | } 24 | 25 | /** 26 | * 发送通过拖拽选择的文件 27 | * 28 | * @param {string[]} filePaths 29 | * @memberof FileService 30 | */ 31 | sendDropFiles(filePaths: string[]) { 32 | this.ipcRender.send('/file/drop', filePaths); 33 | } 34 | 35 | /** 36 | * 发送请求打开文件选择框 37 | * 38 | * @memberof FileService 39 | */ 40 | selectLoaclFiles() { 41 | this.ipcRender.send('/file/select'); 42 | } 43 | 44 | /** 45 | * 获取上传中的文件列表 46 | * 47 | * @returns {Observable} 48 | * @memberof FileService 49 | */ 50 | uploadFileList(): Observable { 51 | return Observable.create((observer: Subscriber) => { 52 | this.ipcRender.on('/file/uploadlist', (e, files: SelectedFile[]) => { 53 | observer.next(files); 54 | }); 55 | }); 56 | } 57 | 58 | /** 59 | * 获取文件上传的进度 60 | * 61 | * @returns {Observable} 62 | * @memberof FileService 63 | */ 64 | uploadProgress(): Observable { 65 | return Observable.create((observer: Subscriber) => { 66 | electron.ipcRenderer.on('/file/upload/progress', (e, { id, progress }) => { 67 | observer.next({ 68 | id, 69 | progress 70 | }); 71 | }); 72 | }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/renderer/app/service/router.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NgZone } from '@angular/core'; 2 | import { CanActivate, ActivatedRouteSnapshot, Router } from '@angular/router'; 3 | 4 | import { BaseService } from './base.service'; 5 | import { SettingService } from './setting.service'; 6 | 7 | @Injectable() 8 | export class RouterService extends BaseService implements CanActivate { 9 | constructor(private settingService: SettingService, private router: Router, private zone: NgZone) { 10 | super(); 11 | 12 | this.ipcRender.on('/open/setting', (e) => { 13 | this.zone.run(() => { 14 | this.router.navigateByUrl('/setting'); 15 | }); 16 | }); 17 | } 18 | 19 | canActivate(route: ActivatedRouteSnapshot) { 20 | if (this.settingService.valiad) { 21 | return true; 22 | } 23 | 24 | this.router.navigateByUrl('/setting'); 25 | return false; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/renderer/app/service/setting.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { BaseService } from './base.service'; 4 | import { Settings } from '../setting/setting.model'; 5 | 6 | @Injectable() 7 | export class SettingService extends BaseService { 8 | private settings: Settings; 9 | constructor() { 10 | super(); 11 | this.reset(); 12 | 13 | this.settings = this.ipcRender.sendSync('/setting/get'); 14 | } 15 | 16 | private reset() { 17 | this.settings = { 18 | ak: '', 19 | sk: '', 20 | scope: '', 21 | domain: '' 22 | }; 23 | } 24 | 25 | private checkSetting(): boolean { 26 | const settings = this.settings; 27 | const len = Object.keys(settings).reduce((prev, cur) => { 28 | if (typeof prev === 'string') { 29 | return settings[prev].length + settings[cur].length; 30 | } 31 | 32 | return prev + settings[cur].length; 33 | }); 34 | 35 | return (parseInt(len, 10) >= 4); 36 | } 37 | 38 | get valiad() { 39 | return this.checkSetting(); 40 | } 41 | 42 | getSetting() { 43 | return this.settings; 44 | } 45 | 46 | saveSetting(settings: Settings) { 47 | this.ipcRender.send('/setting/save', settings); 48 | this.settings = settings; 49 | } 50 | 51 | clearSetting() { 52 | this.ipcRender.send('/setting/clear'); 53 | this.reset(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/renderer/app/setting/setting.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 6 |
7 |
8 | 10 |
11 |
12 | 14 |
15 |
16 | 18 |
19 |
20 | 21 | 22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /src/renderer/app/setting/setting.component.scss: -------------------------------------------------------------------------------- 1 | .wrap.setting { 2 | .field { 3 | padding: .75rem 1.25rem; 4 | &>label { 5 | display: block; 6 | margin-bottom: 1rem; 7 | } 8 | &>input { 9 | width: 100%; 10 | border: 1px solid #efefef; 11 | background: #fff; 12 | border-radius: 2px; 13 | font-size: 14px; 14 | padding: 8px 16px; 15 | &:hover { 16 | border-color: #e3e3e3; 17 | } 18 | &.ng-touched.ng-invalid { 19 | border: 1px solid #a94442; 20 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075) 21 | } 22 | } 23 | &:last-child { 24 | text-align: right; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/renderer/app/setting/setting.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { Setting, Settings } from './setting.model'; 4 | import { SettingService } from '../service/setting.service'; 5 | 6 | @Component({ 7 | selector: 'app-setting', 8 | templateUrl: './setting.component.html', 9 | styleUrls: ['./setting.component.scss'] 10 | }) 11 | export class SettingComponent implements OnInit { 12 | model: Setting; 13 | 14 | constructor(private settingSerivce: SettingService) { 15 | this.model = new Setting(); 16 | } 17 | 18 | ngOnInit() { 19 | this.model.update(this.settingSerivce.getSetting()); 20 | } 21 | 22 | saveSetting() { 23 | this.settingSerivce.saveSetting(this.model.getSetting()); 24 | } 25 | 26 | clearSetting() { 27 | this.settingSerivce.clearSetting(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/renderer/app/setting/setting.model.ts: -------------------------------------------------------------------------------- 1 | export interface Settings { 2 | ak: string; 3 | sk: string; 4 | scope: string; 5 | domain: string; 6 | } 7 | 8 | export class Setting { 9 | public ak: string; 10 | public sk: string; 11 | public scope: string; 12 | public domain: string; 13 | 14 | constructor() { 15 | this.ak = ''; 16 | this.sk = ''; 17 | this.scope = ''; 18 | this.domain = ''; 19 | } 20 | 21 | update({ ak, sk, scope, domain }: Settings) { 22 | this.ak = ak; 23 | this.sk = sk; 24 | this.scope = scope; 25 | this.domain = domain; 26 | } 27 | 28 | getSetting(): Settings { 29 | return { 30 | ak: this.ak, 31 | sk: this.sk, 32 | scope: this.scope, 33 | domain: this.domain 34 | }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/renderer/app/upload/upload-details/upload-details.component.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    4 | {{fileName}} 5 |
    6 |
    {{sizeTxt}} 7 | 8 |
    9 |
  • 10 | -------------------------------------------------------------------------------- /src/renderer/app/upload/upload-details/upload-details.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../assets/mixin.scss"; 2 | 3 | li{ 4 | display: flex; 5 | flex-direction:row; 6 | justify-content: space-between; 7 | padding: 0 8px; 8 | height: 48px; 9 | border-bottom: 1px solid #666; 10 | line-height: 48px; 11 | 12 | div:first-child { 13 | flex-grow: 1; 14 | color: #0789cf; 15 | cursor: pointer; 16 | 17 | user-select: none; 18 | @include ellipsis; 19 | } 20 | 21 | div:last-child { 22 | min-width: 94px; 23 | text-align: right; 24 | 25 | &.completed { 26 | animation:completed .6s ease-in-out .5s forwards; 27 | } 28 | } 29 | 30 | app-progress { 31 | margin-left: 4px; 32 | } 33 | } 34 | 35 | @keyframes completed { 36 | 0% { 37 | transform: translateX(0); 38 | } 39 | 50% { 40 | transform: translateX(40px); 41 | } 42 | 100% { 43 | transform: translateX(34px); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/renderer/app/upload/upload-details/upload-details.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | 3 | import { ProgressItem } from '../upload.component'; 4 | import { SettingService } from '../../service/setting.service'; 5 | 6 | @Component({ 7 | selector: 'app-upload-details', 8 | templateUrl: './upload-details.component.html', 9 | styleUrls: ['./upload-details.component.scss'] 10 | }) 11 | export class UploadDetailsComponent implements OnInit { 12 | @Input() progressItem: ProgressItem; 13 | domian = ''; 14 | sizeTxt = ''; 15 | fileName = ''; 16 | 17 | constructor(private setting: SettingService) { } 18 | 19 | ngOnInit() { 20 | const progressItem = this.progressItem; 21 | this.fileName = progressItem.name + '.' + progressItem.ext; 22 | this.sizeTxt = this.translateSize(progressItem.size); 23 | this.domian = this.setting.getSetting().domain; 24 | } 25 | 26 | getOuterLink() { 27 | electron.clipboard.writeText(`${this.domian}/${this.fileName}`); 28 | } 29 | 30 | private translateSize(size: number) { 31 | const unit = ['b', 'KB', 'MB', 'GB']; 32 | let index = 0; 33 | 34 | while (size >= 1024) { 35 | size = size / 1024; 36 | index++; 37 | } 38 | size = Math.ceil(size); 39 | 40 | return `${size}${unit[index]}`; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/renderer/app/upload/upload.component.html: -------------------------------------------------------------------------------- 1 |
    2 |
    4 |
      5 | 7 |
    8 |
    9 |
    15 |
    16 | 17 | 19 |

    Drag & Drop

    20 |

    your files here

    21 |

    or browse.

    23 |
    24 |
    25 |
    26 | -------------------------------------------------------------------------------- /src/renderer/app/upload/upload.component.scss: -------------------------------------------------------------------------------- 1 | .upload-wrap { 2 | padding: 0 48px 48px; 3 | height: calc(100% - 48px); 4 | .upload-area { 5 | position: relative; 6 | height: 100%; 7 | border: 2px dashed #ccc; 8 | border-radius: 2px; 9 | transition: border-color .3s .05s; 10 | .center { 11 | position: absolute; 12 | top: 50%; 13 | left: 50%; 14 | color: #999; 15 | text-align: center; 16 | transition: color .3s .05s; 17 | transform: translateX(-50%) translateY(-50%); 18 | pointer-events: none; 19 | .btn { 20 | display: none; 21 | } 22 | .icon-upload { 23 | font-size: 8rem; 24 | } 25 | p { 26 | line-height: 1.5; 27 | &.big { 28 | font-weight: 700; 29 | font-size: 2rem; 30 | } 31 | &.small { 32 | color: #999999; 33 | font-size: .75rem; 34 | a { 35 | position: relative; 36 | color: #b6d0e3; 37 | text-decoration: none; 38 | pointer-events: all; 39 | } 40 | } 41 | } 42 | } 43 | &.inline { 44 | height: 56px; 45 | .center { 46 | position: relative; 47 | padding-left: 16px; 48 | text-align: left; 49 | .icon-upload, 50 | .small { 51 | display: none; 52 | } 53 | .btn { 54 | display: inline-block; 55 | } 56 | p { 57 | display: inline; 58 | text-transform: lowercase; 59 | font-size: 1rem; 60 | } 61 | .btn+p:before { 62 | content: " or "; 63 | } 64 | } 65 | } 66 | &.drag-over { 67 | border: 2px solid #0789cf; 68 | .center { 69 | color: #0789cf; 70 | } 71 | } 72 | } 73 | .progerss-area{ 74 | overflow: auto; 75 | margin-bottom: 16px; 76 | padding-right: 8px; 77 | height: calc(100% - 56px); 78 | 79 | ul { 80 | position: relative; 81 | overflow: hidden; 82 | border-top: 1px solid #666666; 83 | } 84 | 85 | &::-webkit-scrollbar{ 86 | width: 10px; 87 | height: 10px; 88 | } 89 | &::-webkit-scrollbar-corner{ 90 | background-color: #535353; 91 | } 92 | &::-webkit-scrollbar-thumb{ 93 | border-radius: 5px; 94 | background-color: #ccc; 95 | } 96 | &::-webkit-scrollbar-track{ 97 | background: 0 0; 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/renderer/app/upload/upload.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, DoCheck, NgZone } from '@angular/core'; 2 | 3 | import { FileService, SelectedFile } from '../service/file.service'; 4 | 5 | export interface ProgressItem { 6 | name: string; 7 | ext: string; 8 | progress: number; 9 | id: number | string; 10 | size?: number; 11 | } 12 | 13 | @Component({ 14 | selector: 'app-upload', 15 | templateUrl: './upload.component.html', 16 | styleUrls: ['./upload.component.scss'] 17 | }) 18 | export class UploadComponent implements OnInit, DoCheck { 19 | dragOver = false; 20 | inline = false; 21 | progressList: ProgressItem[] = []; 22 | 23 | constructor(private fileService: FileService, private zone: NgZone) { 24 | electron.ipcRenderer.on('/file/upload/error', (e, { id, err }) => { 25 | console.log('error', id, err); 26 | }); 27 | } 28 | 29 | ngOnInit() { 30 | this.fileService.uploadFileList() 31 | .subscribe(files => { 32 | const temp = files.map((file) => { 33 | return { 34 | name: file.fileName, 35 | ext: file.ext, 36 | size: file.size, 37 | progress: 0, 38 | id: file.localPath 39 | }; 40 | }); 41 | 42 | this.progressList.unshift(...temp); 43 | }); 44 | 45 | this.fileService.uploadProgress() 46 | .subscribe(({ id, progress }) => { 47 | this.zone.run(() => { 48 | const index = this.findIndexById(id); 49 | const updateItem = this.progressList[index]; 50 | updateItem.progress = progress; 51 | 52 | this.progressList[index] = updateItem; 53 | }); 54 | }); 55 | } 56 | 57 | ngDoCheck() { 58 | this.inline = this.progressList.length !== 0; 59 | } 60 | 61 | drop(e: DragEvent) { 62 | const _files = e.dataTransfer.files; 63 | const filePaths = []; 64 | for (let i = 0, len = _files.length; i < len; i++) { 65 | filePaths.push(_files[i].path); 66 | } 67 | 68 | this.dragOver = false; 69 | this.fileService.sendDropFiles(filePaths); 70 | } 71 | 72 | selectFile() { 73 | this.fileService.selectLoaclFiles(); 74 | } 75 | 76 | private findIndexById(id) { 77 | let index = 0; 78 | for (index = 0; index < this.progressList.length; index++) { 79 | const element = this.progressList[index]; 80 | if (element.id === id) { 81 | break; 82 | } 83 | } 84 | 85 | return index; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/renderer/app/upload/upload.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { ComponentsModule } from '../components/components.module'; 5 | 6 | import { UploadComponent } from './upload.component'; 7 | import { UploadDetailsComponent } from './upload-details/upload-details.component'; 8 | 9 | @NgModule({ 10 | imports: [CommonModule, ComponentsModule], 11 | declarations: [UploadComponent, UploadDetailsComponent], 12 | exports: [UploadComponent] 13 | }) 14 | export class UploadModule { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/renderer/assets/iconfont/iconfont.css: -------------------------------------------------------------------------------- 1 | 2 | @font-face {font-family: "iconfont"; 3 | src: url('iconfont.eot?t=1504007592716'); /* IE9*/ 4 | src: url('iconfont.eot?t=1504007592716#iefix') format('embedded-opentype'), /* IE6-IE8 */ 5 | url('data:application/x-font-woff;charset=utf-8;base64,d09GRgABAAAAAAckAAsAAAAACpQAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAADMAAABCsP6z7U9TLzIAAAE8AAAAQwAAAFZW70iEY21hcAAAAYAAAAB5AAAByJwA0ZdnbHlmAAAB/AAAAwgAAAQQRGuc72hlYWQAAAUEAAAAMQAAADYP9TFMaGhlYQAABTgAAAAgAAAAJAkdBMdobXR4AAAFWAAAABgAAAAYGSn//2xvY2EAAAVwAAAADgAAAA4EOAMsbWF4cAAABYAAAAAfAAAAIAEVAJhuYW1lAAAFoAAAAUUAAAJtPlT+fXBvc3QAAAboAAAAPAAAAE2h7eCDeJxjYGRgYOBikGPQYWB0cfMJYeBgYGGAAJAMY05meiJQDMoDyrGAaQ4gZoOIAgCKIwNPAHicY2BkMWKcwMDKwMHUyXSGgYGhH0IzvmYwYuRgYGBiYGVmwAoC0lxTGBwYKp4lMDf8b2CIYW5gaAQKM4LkAOqYDAgAeJzFkcEJhTAQRN8a/YjYhxfL8CS/BrEK8WC924bOJnqwAie8kBk2bMgCDZDEKGqwAyO0KbWcJ7qc18zyPS2VzrsPPvni63kqfbtHpupnhWt0r4pu9uMz2Xet3+rz/r9dzGG/0RN9KETuUyHm40tB/4ivBdIFLYEb7wAAAHicjVNPaNNQGH/fe31JkyapS9okzdamf7ZEmUatWYvKVsFdJk7dxg7iyYugdsPbPCgrQ0FQnDjw5C7iEA8ybzu4gaInRfDgxZuiiDsIXsSD2vql3WCIB8Mjed/7ff9+v/eFcEJaH9gqs4lBtpO9ZJicIASEfihqNAsFPwxoP6QLPG2lNOaX/IJYKgZsEKyikDLLldCzBFFIggY52FcoV/yA+jAQDtGDUDazAJluZ0Lv69HZLZBtP3e1eYTeg7Rb6kkO7WqO7KylynkjPqPoekbXb8QFzuOUxpIaTFmmxCVZaN7nSSe96u6gLigZ3zl6Us1366evhdPZPksCaDTA6M5rD2pdTheuS45p6Blxmxq3HbXUm4KZTwnbULLeR4IPRa5P2Qt2mMyS68gy9JCmtxu8Sg0qIX6LEZHdEIA/0MaELKSQnOB1MCHlIs3IZwMzI5YumEIHM8s1GILIZwOrDKKNuc0OFtUIIPKJsIE+DUQsNQTVHLDnuhIkM9wSuZyXpGrciRcSrnpANUBXjim6oe3XcokCHlclKS9z0Y45WoC6KYHmxOwtUbKLrkY7CAxM4G6Nsng7qtli9bHxOmP18bE6PEwYmP5vx0yyk/6fTRmJ4+2e1Lz8Xz3l1XZPO8brlGLNKUqn8D6g1WrN8kOsQSbRQClQEA3SObD2RZPVHx2FleqGURTbSonpaO4GUWQL3UuCV+2yokHcvKJI6K5IffQOIFZLvHu19iNx5nxsaeaNKEnim3MLrNHYE2332Jb89tnrFyBp0uhNzhbOfRsdhjmuynxes7V5Lqt8bvLUt4tLjN+F90vfhdjXJ72PJi4vxzCCLV4YW4njJnbniyD+WuyklOYvnn4owPAorMxxWcFEmEeR+Vxz5NSksDY1+6Azh5/ZMnNIgvQg80LRCyv495gpoQ/wVfTBC6tQwQORXfn907Btg3Ldtn+X4ITqlNTmbVWFabXkqDCZ6c3g6m8+bpvTWlFDBH0ifdtDv07PYh0CvuiLlmhVrapP192XLzcXPbLF+ANOHqy5eJxjYGRgYADi2O8ca+P5bb4ycLMwgMDV02UrYPT////rWR2YG4BcDgYmkCgAaPYNhAAAAHicY2BkYGBu+N/AEMPq8P8/AwOrAwNQBAWwAQCBCwTvBAAAAAPpAAAEAAAABUD//wQAAAAEAAAAAAAAAAB2AT4BugHuAggAAHicY2BkYGBgY+hhYGUAASYg5gJCBob/YD4DABcLAa4AeJxlj01OwzAQhV/6B6QSqqhgh+QFYgEo/RGrblhUavdddN+mTpsqiSPHrdQDcB6OwAk4AtyAO/BIJ5s2lsffvHljTwDc4Acejt8t95E9XDI7cg0XuBeuU38QbpBfhJto41W4Rf1N2MczpsJtdGF5g9e4YvaEd2EPHXwI13CNT+E69S/hBvlbuIk7/Aq30PHqwj7mXle4jUcv9sdWL5xeqeVBxaHJIpM5v4KZXu+Sha3S6pxrW8QmU4OgX0lTnWlb3VPs10PnIhVZk6oJqzpJjMqt2erQBRvn8lGvF4kehCblWGP+tsYCjnEFhSUOjDFCGGSIyujoO1Vm9K+xQ8Jee1Y9zed0WxTU/3OFAQL0z1xTurLSeTpPgT1fG1J1dCtuy56UNJFezUkSskJe1rZUQuoBNmVXjhF6XNGJPyhnSP8ACVpuyAAAAHicY2BigAAuBuyAjZGJkZmRhZGVkY2RnYGxgr04taQkMy+drbQgJz8xhSUpMTmbNTknvziVgQEApREJ7Q==') format('woff'), 6 | url('iconfont.ttf?t=1504007592716') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ 7 | url('iconfont.svg?t=1504007592716#iconfont') format('svg'); /* iOS 4.1- */ 8 | } 9 | 10 | .iconfont { 11 | font-family:"iconfont" !important; 12 | font-size:16px; 13 | font-style:normal; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | } 17 | 18 | .icon-setting:before { content: "\e65e"; } 19 | 20 | .icon-upload:before { content: "\e660"; } 21 | 22 | .icon-back:before { content: "\e644"; } 23 | 24 | .icon-close:before { content: "\e627"; } 25 | 26 | -------------------------------------------------------------------------------- /src/renderer/assets/iconfont/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lleohao/qiniu-upload/6073e8b28b577c0a1a58f70b5a523782417c3e7f/src/renderer/assets/iconfont/iconfont.eot -------------------------------------------------------------------------------- /src/renderer/assets/iconfont/iconfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Created by iconfont 9 | 10 | 11 | 12 | 13 | 21 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/renderer/assets/iconfont/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lleohao/qiniu-upload/6073e8b28b577c0a1a58f70b5a523782417c3e7f/src/renderer/assets/iconfont/iconfont.ttf -------------------------------------------------------------------------------- /src/renderer/assets/iconfont/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lleohao/qiniu-upload/6073e8b28b577c0a1a58f70b5a523782417c3e7f/src/renderer/assets/iconfont/iconfont.woff -------------------------------------------------------------------------------- /src/renderer/assets/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin ellipsis { 2 | overflow: hidden; 3 | text-overflow: ellipsis; 4 | white-space: nowrap; 5 | word-break: keep-all; 6 | } 7 | -------------------------------------------------------------------------------- /src/renderer/assets/reset.css: -------------------------------------------------------------------------------- 1 | /* 2 | html5doctor.com Reset Stylesheet 3 | v1.6.1 4 | Last Updated: 2010-09-17 5 | Author: Richard Clark - http://richclarkdesign.com 6 | Twitter: @rich_clark 7 | */ 8 | 9 | html { 10 | box-sizing: border-box; 11 | } 12 | 13 | *, 14 | *:before, 15 | *:after { 16 | box-sizing: inherit; 17 | } 18 | 19 | html, 20 | body, 21 | div, 22 | span, 23 | object, 24 | iframe, 25 | h1, 26 | h2, 27 | h3, 28 | h4, 29 | h5, 30 | h6, 31 | p, 32 | blockquote, 33 | pre, 34 | abbr, 35 | address, 36 | cite, 37 | code, 38 | del, 39 | dfn, 40 | em, 41 | img, 42 | ins, 43 | kbd, 44 | q, 45 | samp, 46 | small, 47 | strong, 48 | sub, 49 | sup, 50 | var, 51 | b, 52 | i, 53 | dl, 54 | dt, 55 | dd, 56 | ol, 57 | ul, 58 | li, 59 | fieldset, 60 | form, 61 | label, 62 | legend, 63 | table, 64 | caption, 65 | tbody, 66 | tfoot, 67 | thead, 68 | tr, 69 | th, 70 | td, 71 | article, 72 | aside, 73 | canvas, 74 | details, 75 | figcaption, 76 | figure, 77 | footer, 78 | header, 79 | hgroup, 80 | menu, 81 | nav, 82 | section, 83 | summary, 84 | time, 85 | mark, 86 | audio, 87 | video { 88 | margin: 0; 89 | padding: 0; 90 | border: 0; 91 | outline: 0; 92 | font-size: 100%; 93 | vertical-align: baseline; 94 | background: transparent; 95 | } 96 | 97 | body { 98 | line-height: 1; 99 | } 100 | 101 | article, 102 | aside, 103 | details, 104 | figcaption, 105 | figure, 106 | footer, 107 | header, 108 | hgroup, 109 | menu, 110 | nav, 111 | section { 112 | display: block; 113 | } 114 | 115 | nav ul { 116 | list-style: none; 117 | } 118 | 119 | blockquote, 120 | q { 121 | quotes: none; 122 | } 123 | 124 | blockquote:before, 125 | blockquote:after, 126 | q:before, 127 | q:after { 128 | content: ''; 129 | content: none; 130 | } 131 | 132 | a { 133 | margin: 0; 134 | padding: 0; 135 | font-size: 100%; 136 | vertical-align: baseline; 137 | background: transparent; 138 | } 139 | 140 | html, 141 | body { 142 | height: 100%; 143 | width: 100%; 144 | } 145 | 146 | body { 147 | font-family: 'microsoft yahei ui', 'microsoft yahei', PingFangSC-Light, 'helvetica neue', 'hiragino sans gb', arial, simsun, sans-serif; 148 | } 149 | -------------------------------------------------------------------------------- /src/renderer/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/renderer/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /src/renderer/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lleohao/qiniu-upload/6073e8b28b577c0a1a58f70b5a523782417c3e7f/src/renderer/favicon.ico -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Upload 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/renderer/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule); 12 | -------------------------------------------------------------------------------- /src/renderer/polyfills.ts: -------------------------------------------------------------------------------- 1 | import 'core-js/es6/reflect'; 2 | import 'core-js/es7/reflect'; 3 | import 'zone.js/dist/zone'; 4 | -------------------------------------------------------------------------------- /src/renderer/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import "assets/iconfont/iconfont.css"; 3 | @import "assets/reset.css"; 4 | .wrap { 5 | padding: 0 16px 16px; 6 | height: calc(100% - 48px); 7 | background-color: #fff; 8 | } 9 | 10 | .info { 11 | margin-bottom: 1rem; 12 | padding: .75rem 1.25rem; 13 | border: 1px solid transparent; 14 | border-color: #b8daff; 15 | border-radius: .25rem; 16 | background-color: #cce5ff; 17 | color: #004085; 18 | } 19 | 20 | .btn { 21 | padding: 6px 16px; 22 | border: 1px solid #0167c6; 23 | border-radius: 2px; 24 | background: #0174df; 25 | color: #fff; 26 | text-align: center; 27 | font-size: 1rem; 28 | cursor: pointer; 29 | pointer-events: all; 30 | &.btn-grey { 31 | border: 1px solid #ccc; 32 | background-color: #fff; 33 | color: #666666; 34 | &:hover { 35 | border-color: #999; 36 | background: #fff; 37 | } 38 | } 39 | &:hover { 40 | border-color: #0181f8; 41 | background: #0181f8; 42 | } 43 | &:disabled { 44 | opacity: .65; 45 | cursor: not-allowed; 46 | } 47 | &:active, 48 | &:focus { 49 | outline: none; 50 | } 51 | } 52 | 53 | .iconfont { 54 | cursor: pointer; 55 | } 56 | -------------------------------------------------------------------------------- /src/renderer/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out/app", 5 | "baseUrl": "./", 6 | "module": "es2015", 7 | "types": [] 8 | }, 9 | "exclude": [ 10 | "test.ts", 11 | "**/*.spec.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/renderer/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | /// 3 | declare var module: NodeModule; 4 | interface NodeModule { 5 | id: string; 6 | } 7 | declare var electron: Electron.AllElectron; 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "sourceMap": true, 5 | "declaration": false, 6 | "moduleResolution": "node", 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "target": "es5", 10 | "typeRoots": [ 11 | "node_modules/@types" 12 | ], 13 | "lib": [ 14 | "es2017", 15 | "dom" 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "eofline": true, 15 | "forin": true, 16 | "import-blacklist": [ 17 | true, 18 | "rxjs" 19 | ], 20 | "import-spacing": true, 21 | "indent": [ 22 | true, 23 | "spaces" 24 | ], 25 | "interface-over-type-literal": true, 26 | "label-position": true, 27 | "max-line-length": [ 28 | true, 29 | 140 30 | ], 31 | "member-access": false, 32 | "member-ordering": [ 33 | true, 34 | { 35 | "order": [ 36 | "static-field", 37 | "instance-field", 38 | "static-method", 39 | "instance-method" 40 | ] 41 | } 42 | ], 43 | "no-arg": true, 44 | "no-bitwise": true, 45 | "no-console": [ 46 | true, 47 | "debug", 48 | "info", 49 | "time", 50 | "timeEnd", 51 | "trace" 52 | ], 53 | "no-construct": true, 54 | "no-debugger": true, 55 | "no-duplicate-super": true, 56 | "no-empty": false, 57 | "no-empty-interface": true, 58 | "no-eval": true, 59 | "no-inferrable-types": [ 60 | true, 61 | "ignore-params" 62 | ], 63 | "no-misused-new": true, 64 | "no-non-null-assertion": true, 65 | "no-shadowed-variable": true, 66 | "no-string-literal": false, 67 | "no-string-throw": true, 68 | "no-switch-case-fall-through": true, 69 | "no-trailing-whitespace": [ 70 | true, 71 | "ignore-comments", 72 | "ignore-jsdoc" 73 | ], 74 | "no-unnecessary-initializer": true, 75 | "no-unused-expression": true, 76 | "no-use-before-declare": true, 77 | "no-var-keyword": true, 78 | "object-literal-sort-keys": false, 79 | "one-line": [ 80 | true, 81 | "check-open-brace", 82 | "check-catch", 83 | "check-else", 84 | "check-whitespace" 85 | ], 86 | "prefer-const": true, 87 | "quotemark": [ 88 | true, 89 | "single" 90 | ], 91 | "radix": true, 92 | "semicolon": [ 93 | true, 94 | "always" 95 | ], 96 | "triple-equals": [ 97 | true, 98 | "allow-null-check" 99 | ], 100 | "typedef-whitespace": [ 101 | true, 102 | { 103 | "call-signature": "nospace", 104 | "index-signature": "nospace", 105 | "parameter": "nospace", 106 | "property-declaration": "nospace", 107 | "variable-declaration": "nospace" 108 | } 109 | ], 110 | "typeof-compare": true, 111 | "unified-signatures": true, 112 | "variable-name": false, 113 | "whitespace": [ 114 | true, 115 | "check-branch", 116 | "check-decl", 117 | "check-operator", 118 | "check-separator", 119 | "check-type" 120 | ], 121 | "directive-selector": [ 122 | true, 123 | "attribute", 124 | "app", 125 | "camelCase" 126 | ], 127 | "component-selector": [ 128 | true, 129 | "element", 130 | "app", 131 | "kebab-case" 132 | ], 133 | "use-input-property-decorator": true, 134 | "use-output-property-decorator": true, 135 | "use-host-property-decorator": true, 136 | "no-input-rename": true, 137 | "no-output-rename": true, 138 | "use-life-cycle-interface": true, 139 | "use-pipe-transform-interface": true, 140 | "component-class-suffix": true, 141 | "directive-class-suffix": true, 142 | "no-access-missing-member": true, 143 | "templates-use-public": true, 144 | "invoke-injectable": true 145 | } 146 | } 147 | --------------------------------------------------------------------------------