├── .github └── workflows │ └── build_and_release.yml ├── .gitignore ├── .nvmrc ├── LICENSE ├── README.md ├── build ├── background.png ├── icon.icns ├── icon.ico └── icon.png ├── lib └── app.js ├── package.json ├── screenshots ├── 01.png ├── 02.png └── 03.png ├── source ├── index.html ├── javascripts │ ├── all.coffee │ ├── api.js │ ├── index.js │ └── vendors.js └── stylesheets │ ├── all.sass │ └── loaders.less ├── webpack.config.js └── yarn.lock /.github/workflows/build_and_release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | tags: 8 | - "v*" 9 | jobs: 10 | build_linux: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: '12.x' 17 | - run: yarn install 18 | - run: yarn release 19 | env: 20 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | build_windows: 22 | runs-on: windows-latest 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: actions/setup-node@v1 26 | with: 27 | node-version: '12.x' 28 | - run: yarn install 29 | - run: yarn release 30 | env: 31 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | build_macos: 33 | runs-on: macos-latest 34 | steps: 35 | - uses: actions/checkout@v2 36 | - uses: actions/setup-node@v1 37 | with: 38 | node-version: '12.x' 39 | - run: yarn install 40 | - run: yarn release 41 | env: 42 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | bundle 4 | .sass-cache 5 | dist 6 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v10.16 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Michael Yin 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 | manager-for-upyun 2 | ===== 3 | 4 | manager-for-upyun 是一个又拍云资源管理器,使用它您可以上传、下载和管理您储存于又拍云空间中的文件资源。 5 | 6 | 又拍云是一个非结构化数据云存储、云处理、云分发平台,关于又拍云的详细介绍请参见[又拍云官方网站](https://www.upyun.com/)。没有又拍云帐号?可以使用这个公用的演示帐号登录: 7 | 8 | ``` 9 | 操作员:layerssss 10 | 操作员密码:MD5_f92a36b6eb4d964c1b64cc008ecac009 11 | 空间名:manager-for-upyun 12 | ``` 13 | 14 | 可以直接在打开以下地址来登录演示空间: 15 | 16 | ``` 17 | upyun://layerssss:MD5_f92a36b6eb4d964c1b64cc008ecac009@manager-for-upyun/ 18 | ``` 19 | 20 | 下载 21 | ------ 22 | 23 | * 当前版本: 0.0.9 (更新于2019-08-28) 24 | * [Windows / Mac / Linux](https://github.com/layerssss/manager-for-upyun/releases) 25 | 26 | ![01.png](screenshots/01.png) 27 | ![02.png](screenshots/02.png) 28 | ![03.png](screenshots/03.png) 29 | 30 | 发布说明 31 | ------ 32 | 33 | 0.0.9 (更新于2019-08-28) 34 | 35 | * 增加对自定义域名的支持 36 | 37 | 0.0.8 (更新于2017-09-15) 38 | 39 | * 修复无法将文件公共地址复制到剪切板的问题 40 | 41 | 0.0.7 (更新于2017-08-22) 42 | 43 | * 将 node-webkit 更换为 Electron 44 | * 将 middleman 更换为 webpack 45 | 46 | 0.0.6 (更新于2014-06-01) 47 | 48 | * 修改登录界面的外观 49 | * 升级至 node-webkit 0.9.2 修正了文件夹选定的问题,并增强了稳定性 50 | 51 | 0.0.5 (更新于2014-05-26) 52 | 53 | * 显示单个文件上传的进度 54 | * (Windows) 可以分享和打开特定目录的 upyun://... 地址 55 | * 可以将公共地址复制到剪切板 56 | * 可以创建文件 57 | * 不再使用 SSL 接入点,并且使用了摘要授权 58 | * 在工具栏上加入下载和上传的按钮 59 | 60 | 0.0.3 (更新于2014-04-28) 61 | 62 | * 完成文件和文件夹的拖拽上传 63 | * 完成文件和文件夹的下载 64 | * 基本的收藏夹功能 65 | 66 | 功能开发路线 67 | ------ 68 | 69 | - [x] 收藏夹 70 | - [x] 文件浏览 71 | - [x] 文件删除 72 | - [x] 下载整个目录或单个文件 73 | - [x] 拖拽上传整个目录或单个文件 74 | - [x] 可以取消、看到进度、多个请求同时进行的操作 75 | - [x] 快速筛选列表 76 | - [x] 快速编辑文本文件 77 | - [x] 递归删除目录里的内容以确保成功删除 78 | - [x] 通过类似`upyun://...`的链接打开 & 分享特定目录 79 | - [ ] 美化界面,以和又拍云管理面板的视觉风格统一 80 | - [ ] 更新提示 81 | - [ ] 移动、重命名文件和目录 82 | 83 | 使用的技术 84 | ------ 85 | 86 | * Electron 87 | * webpack 88 | * ace editor 89 | 90 | 源码授权 91 | ------ 92 | 93 | MIT(详见源代码中的 [LICENSE](LICENSE)) 94 | -------------------------------------------------------------------------------- /build/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/layerssss/manager-for-upyun/d3e94d346bf2dba390f9a05ef53c26a59505f44f/build/background.png -------------------------------------------------------------------------------- /build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/layerssss/manager-for-upyun/d3e94d346bf2dba390f9a05ef53c26a59505f44f/build/icon.icns -------------------------------------------------------------------------------- /build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/layerssss/manager-for-upyun/d3e94d346bf2dba390f9a05ef53c26a59505f44f/build/icon.ico -------------------------------------------------------------------------------- /build/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/layerssss/manager-for-upyun/d3e94d346bf2dba390f9a05ef53c26a59505f44f/build/icon.png -------------------------------------------------------------------------------- /lib/app.js: -------------------------------------------------------------------------------- 1 | const Electron = require('electron'); 2 | const ElectronUpdater = require('electron-updater'); 3 | const ElectronPrompt = require('electron-prompt'); 4 | const Path = require('path'); 5 | const URL = require('url'); 6 | const Request = require('request'); 7 | const Mkdirp = require('mkdirp'); 8 | const Fs = require('fs'); 9 | 10 | class App { 11 | constructor() { 12 | 13 | this.window = new Electron.BrowserWindow({ 14 | webPreferences: { 15 | nodeIntegration: true 16 | } 17 | }); 18 | 19 | this.window.loadURL(URL.format({ 20 | pathname: Path.join(__dirname, '..', 'source', 'index.html'), 21 | protocol: 'file:', 22 | slashes: true 23 | })); 24 | 25 | this.window.webContents.on('did-finish-load', () => { 26 | this.window.webContents.send('init-params', { 27 | action: 'login', 28 | params: {} 29 | }); 30 | }); 31 | } 32 | 33 | static HandleAPIs() { 34 | const requests = {}; 35 | 36 | Electron.ipcMain.on('api', (evt, { 37 | id, 38 | apiName, 39 | apiOptions 40 | }) => { 41 | 42 | if (apiName == 'request') { 43 | const { 44 | options, 45 | pipeRequest, 46 | pipeResponse 47 | } = apiOptions; 48 | 49 | const request = requests[id] = Request(options, (error, res, data) => { 50 | evt.sender.send('api-event', { 51 | id, 52 | eventName: 'onFinish', 53 | args: [ 54 | error && { 55 | message: error.message 56 | }, 57 | res && { 58 | statusCode: res.statusCode 59 | }, 60 | data 61 | ] 62 | }) 63 | }) 64 | 65 | if (pipeRequest) { 66 | const readStream = Fs.createReadStream(pipeRequest); 67 | readStream.pipe(request); 68 | readStream.on('data', data => { 69 | evt.sender.send('api-event', { 70 | id, 71 | eventName: 'onRequestData', 72 | args: [data] 73 | }); 74 | }); 75 | } 76 | 77 | if (pipeResponse) { 78 | const writeStream = Fs.createWriteStream(pipeResponse); 79 | request.pipe(writeStream); 80 | writeStream.on('error', error => { 81 | evt.sender.send('api-event', { 82 | id, 83 | eventName: 'onFinish', 84 | args: [error] 85 | }); 86 | 87 | // TODO: make sure request aborts in case of error 88 | }); 89 | } 90 | 91 | request.on('data', (data) => { 92 | evt.sender.send('api-event', { 93 | id, 94 | eventName: 'onData', 95 | args: [data] 96 | }); 97 | }); 98 | } 99 | 100 | if (apiName == 'request-abort') { 101 | if (!requests[id]) return; 102 | requests[id].abort(); 103 | } 104 | 105 | if (apiName == 'mkdirp') { 106 | const { 107 | path 108 | } = apiOptions; 109 | Mkdirp(path, error => { 110 | evt.sender.send('api-event', { 111 | id, 112 | eventName: 'callback', 113 | args: [error && { 114 | message: error.message 115 | }] 116 | }); 117 | }); 118 | } 119 | 120 | if (apiName == 'prompt') { 121 | const { 122 | title, 123 | } = apiOptions; 124 | ElectronPrompt({ 125 | title: title 126 | }).then(path => { 127 | if (!path) return; 128 | evt.sender.send('api-event', { 129 | id, 130 | eventName: 'callback', 131 | args: [path] 132 | }); 133 | }).catch(error => {}); 134 | } 135 | }); 136 | } 137 | } 138 | 139 | ElectronUpdater.autoUpdater.on('update-downloaded', () => { 140 | ElectronUpdater.quitAndInstall(); 141 | }); 142 | 143 | Electron.app.on('ready', () => { 144 | App.HandleAPIs(); 145 | ElectronUpdater.autoUpdater.checkForUpdates(); 146 | new App(); 147 | 148 | Electron.Menu.setApplicationMenu(Electron.Menu.buildFromTemplate([{ 149 | label: "Application", 150 | submenu: [{ 151 | label: "About Application", 152 | selector: "orderFrontStandardAboutPanel:" 153 | }, 154 | { 155 | type: "separator" 156 | }, 157 | { 158 | label: "Quit", 159 | accelerator: "Command+Q", 160 | click: function() { 161 | Electron.app.quit(); 162 | } 163 | } 164 | ] 165 | }, { 166 | label: "Edit", 167 | submenu: [{ 168 | label: "Undo", 169 | accelerator: "CmdOrCtrl+Z", 170 | selector: "undo:" 171 | }, 172 | { 173 | label: "Redo", 174 | accelerator: "Shift+CmdOrCtrl+Z", 175 | selector: "redo:" 176 | }, 177 | { 178 | type: "separator" 179 | }, 180 | { 181 | label: "Cut", 182 | accelerator: "CmdOrCtrl+X", 183 | selector: "cut:" 184 | }, 185 | { 186 | label: "Copy", 187 | accelerator: "CmdOrCtrl+C", 188 | selector: "copy:" 189 | }, 190 | { 191 | label: "Paste", 192 | accelerator: "CmdOrCtrl+V", 193 | selector: "paste:" 194 | }, 195 | { 196 | label: "Select All", 197 | accelerator: "CmdOrCtrl+A", 198 | selector: "selectAll:" 199 | }, 200 | { 201 | label: 'Developer Tools', 202 | accelerator: "CmdOrCtrl+Alt+I", 203 | click: (item, focusedWindow) => { 204 | if (focusedWindow) { 205 | const webContents = focusedWindow.webContents; 206 | if (webContents.isDevToolsOpened()) { 207 | webContents.closeDevTools(); 208 | } else { 209 | webContents.openDevTools({ 210 | mode: 'detach' 211 | }); 212 | } 213 | } 214 | } 215 | } 216 | ] 217 | }])); 218 | }); 219 | 220 | Electron.app.on('window-all-closed', () => { 221 | process.exit(); 222 | }); 223 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "manager-for-upyun", 3 | "version": "0.0.10", 4 | "author": { 5 | "name": "Michael Yin", 6 | "email": "layerssss@gmail.com" 7 | }, 8 | "description": "又拍云资源管理器", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/layerssss/manager-for-upyun.git" 12 | }, 13 | "scripts": { 14 | "app": "electron .", 15 | "release": "webpack && electron-builder build", 16 | "dist": "webpack && electron-builder build", 17 | "watch": "webpack --watch" 18 | }, 19 | "main": "lib/app.js", 20 | "build": { 21 | "appId": "in.micy.managerforupyun", 22 | "productName": "Upyun Manager", 23 | "mac": { 24 | "category": "public.app-category.developer-tools" 25 | }, 26 | "protocols": [ 27 | { 28 | "name": "UpYun URL", 29 | "schemes": [ 30 | "upyun" 31 | ] 32 | } 33 | ], 34 | "linux": { 35 | "target": [ 36 | "deb", 37 | "AppImage", 38 | "rpm" 39 | ], 40 | "category": "Development" 41 | }, 42 | "win": { 43 | "target": [ 44 | "nsis" 45 | ] 46 | } 47 | }, 48 | "license": "MIT", 49 | "dependencies": { 50 | "electron-prompt": "^0.3.0", 51 | "electron-updater": "^4.1.2", 52 | "mkdirp": "^0.3.5", 53 | "request": "^2.34.0" 54 | }, 55 | "devDependencies": { 56 | "ace-builds": "^1.2.8", 57 | "async": "^0.7.0", 58 | "bootstrap": "^3.3.7", 59 | "bootswatch": "^3.3.7", 60 | "coffee-loader": "^0.7.3", 61 | "coffee-script": "^1.12.7", 62 | "css-loader": "^0.28.4", 63 | "electron": "^6.0.5", 64 | "electron-builder": "^22.6.1", 65 | "exports-loader": "^0.6.4", 66 | "extract-text-webpack-plugin": "^3.0.0", 67 | "font-awesome": "^4.7.0", 68 | "form-serializer": "^2.5.0", 69 | "jquery": "^2.2.4", 70 | "less": "^2.7.2", 71 | "less-loader": "^4.0.5", 72 | "md5": "^2.2.1", 73 | "messenger": "git+https://github.com/HubSpot/messenger.git#v1.4.1", 74 | "moment": "^2.6.0", 75 | "node-sass": "^4.5.3", 76 | "sass-loader": "^6.0.6", 77 | "style-loader": "^0.18.2", 78 | "underscore": "^1.8.3", 79 | "underscore.string": "^3.3.4", 80 | "url-loader": "^0.5.9", 81 | "webpack": "^3.5.4" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /screenshots/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/layerssss/manager-for-upyun/d3e94d346bf2dba390f9a05ef53c26a59505f44f/screenshots/01.png -------------------------------------------------------------------------------- /screenshots/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/layerssss/manager-for-upyun/d3e94d346bf2dba390f9a05ef53c26a59505f44f/screenshots/02.png -------------------------------------------------------------------------------- /screenshots/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/layerssss/manager-for-upyun/d3e94d346bf2dba390f9a05ef53c26a59505f44f/screenshots/03.png -------------------------------------------------------------------------------- /source/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 又拍云资源管理器 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |
19 | 20 |
21 |
22 |
23 | 24 | 25 |
26 |
27 | 28 | 29 |
30 |
31 | 32 | 33 |
34 |
35 | 36 | 37 |
38 |
39 | 40 | 41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | 49 | 50 | 51 |
52 | 56 | 70 |
71 |
72 | 76 | 90 |
91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 |
102 | 103 |
104 |
105 |
106 |
107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 |
尺寸最后修改时间
116 |
117 |
118 | 134 |
135 |
136 |
137 |
Loading...
138 |
139 |
140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /source/javascripts/all.coffee: -------------------------------------------------------------------------------- 1 | moment.locale 'zh-cn' 2 | 3 | try 4 | @m_favs = JSON.parse(localStorage.favs)||{} 5 | catch 6 | @m_favs = {} 7 | @refresh_favs = => 8 | $ '#favs' 9 | .empty() 10 | for k, fav of @m_favs 11 | fav.origin ?= "http://#{fav.bucket}.b0.upaiyun.com" 12 | $ li = document.createElement 'li' 13 | .appendTo '#favs' 14 | $ document.createElement 'a' 15 | .appendTo li 16 | .text "#{k} (#{fav.origin})" 17 | .attr href: '#' 18 | .data 'fav', fav 19 | .click (ev)=> 20 | ev.preventDefault() 21 | $(ev.currentTarget).parent().addClass('active').siblings().removeClass 'active' 22 | for k, v of $(ev.currentTarget).data 'fav' 23 | $ "#input#{_.capitalize k}" 24 | .val v 25 | $ '#formLogin' 26 | .submit() 27 | $ document.createElement 'button' 28 | .appendTo li 29 | .prepend @createIcon 'trash-o' 30 | .addClass 'btn btn-danger btn-xs' 31 | .attr 'title', '删除这条收藏记录' 32 | .tooltip placement: 'bottom' 33 | .data 'fav', fav 34 | .click (ev)=> 35 | fav = $(ev.currentTarget).data 'fav' 36 | delete @m_favs["#{fav.username}@#{fav.bucket}"] 37 | localStorage.favs = JSON.stringify @m_favs 38 | @refresh_favs() 39 | @humanFileSize = (bytes, si) -> 40 | thresh = (if si then 1000 else 1024) 41 | return bytes + " B" if bytes < thresh 42 | units = (if si then [ 43 | "kB" 44 | "MB" 45 | "GB" 46 | "TB" 47 | "PB" 48 | "EB" 49 | "ZB" 50 | "YB" 51 | ] else [ 52 | "KiB" 53 | "MiB" 54 | "GiB" 55 | "TiB" 56 | "PiB" 57 | "EiB" 58 | "ZiB" 59 | "YiB" 60 | ]) 61 | u = -1 62 | loop 63 | bytes /= thresh 64 | ++u 65 | break unless bytes >= thresh 66 | bytes.toFixed(1) + " " + units[u] 67 | 68 | @_upyun_api = (opt, cb)=> 69 | opt.headers?= {} 70 | opt.headers["Content-Length"] = String opt.length || opt.data?.length || 0 unless opt.method in ['GET', 'HEAD'] 71 | date = new Date().toUTCString() 72 | if @password.match /^MD5_/ 73 | md5_password = @password.replace /^MD5_/, '' 74 | else 75 | md5_password = MD5 @password 76 | signature = "#{opt.method}&/#{@bucket}#{opt.url}&#{date}" 77 | signature = Crypto.createHmac('sha1', md5_password).update(signature).digest('base64') 78 | 79 | opt.headers["Authorization"] = "UPYUN #{@username}:#{signature}" 80 | opt.headers["Date"] = date 81 | @_upyun_api_req = req = CallRequest 82 | options: 83 | method: opt.method 84 | url: "http://v0.api.upyun.com/#{@bucket}#{opt.url}" 85 | headers: opt.headers 86 | body: opt.data 87 | onData: opt.onData 88 | onRequestData: opt.onRequestData 89 | pipeRequest: opt.pipeRequest 90 | pipeResponse: opt.pipeResponse 91 | onFinish: (e, res, data)=> 92 | return cb e if e 93 | if res.statusCode == 200 94 | cb null, data 95 | else 96 | status = null 97 | try 98 | status = JSON.parse(data) if data 99 | if status && status.msg 100 | cb new Error status.msg 101 | else 102 | cb new Error "未知错误 (HTTP#{res.statusCode})" 103 | return => 104 | req.abort() 105 | cb new Error '操作已取消' 106 | 107 | @upyun_api = (opt, cb)=> 108 | start = Date.now() 109 | @_upyun_api opt, (e, data)=> 110 | console?.log "#{opt.method} #{@bucket}#{opt.url} done (+#{Date.now() - start}ms)" 111 | cb e, data 112 | 113 | Messenger.options = 114 | extraClasses: 'messenger-fixed messenger-on-bottom messenger-on-right' 115 | theme: 'future' 116 | messageDefaults: 117 | showCloseButton: true 118 | hideAfter: 10 119 | retry: 120 | label: '重试' 121 | phrase: 'TIME秒钟后重试' 122 | auto: true 123 | delay: 5 124 | @createIcon = (icon)=> 125 | $ document.createElement 'i' 126 | .addClass "fa fa-#{icon}" 127 | @shortOperation = (title, operation)=> 128 | @shortOperationBusy = true 129 | $ '#loadingText' 130 | .text title||'' 131 | .append '
' 132 | $ 'body' 133 | .addClass 'loading' 134 | $ btnCancel = document.createElement 'button' 135 | .appendTo '#loadingText' 136 | .addClass 'btn btn-default btn-xs btn-inline' 137 | .text '取消' 138 | .hide() 139 | operationDone = (e)=> 140 | @shortOperationBusy = false 141 | $ 'body' 142 | .removeClass 'loading' 143 | if e 144 | msg = Messenger().post 145 | type: 'error' 146 | message: e.message 147 | showCloseButton: true 148 | actions: 149 | ok: 150 | label: '确定' 151 | action: => 152 | msg.hide() 153 | retry: 154 | label: '重试' 155 | action: => 156 | msg.hide() 157 | @shortOperation title, operation 158 | operation operationDone, $ btnCancel 159 | 160 | @taskOperation = (title, operation)=> 161 | msg = Messenger( 162 | instance: @messengerTasks 163 | extraClasses: 'messenger-fixed messenger-on-left messenger-on-bottom' 164 | ).post 165 | hideAfter: 0 166 | message: title 167 | actions: 168 | cancel: 169 | label: '取消' 170 | action: => 171 | $progresslabel1 = $ document.createElement 'span' 172 | .appendTo msg.$message.find('.messenger-message-inner') 173 | .addClass 'pull-right' 174 | $progressbar = $ document.createElement 'div' 175 | .appendTo msg.$message.find('.messenger-message-inner') 176 | .addClass 'progress progress-striped active' 177 | .css margin: 0 178 | .append $(document.createElement 'div').addClass('progress-bar').width '100%' 179 | .append $(document.createElement 'div').addClass('progress-bar progress-bar-success').width '0%' 180 | $progresslabel2 = $ document.createElement 'div' 181 | .appendTo msg.$message.find('.messenger-message-inner') 182 | operationProgress = (progress, progresstext)=> 183 | $progresslabel2.text progresstext if progresstext 184 | $progresslabel1.text "#{progress}%" if progress? 185 | $progressbar 186 | .toggleClass 'active', !progress? 187 | $progressbar.children ':not(.progress-bar-success)' 188 | .toggle !progress? 189 | $progressbar.children '.progress-bar-success' 190 | .toggle progress? 191 | .width "#{progress}%" 192 | operationDone = (e)=> 193 | return unless msg 194 | if e 195 | msg.update 196 | type: 'error' 197 | message: e.message 198 | showCloseButton: true 199 | actions: 200 | ok: 201 | label: '确定' 202 | action: => 203 | msg.hide() 204 | retry: 205 | label: '重试' 206 | action: => 207 | msg.hide() 208 | @taskOperation title, operation 209 | else 210 | msg.hide() 211 | operationProgress null 212 | operation operationProgress, operationDone, msg.$message.find('[data-action="cancel"] a') 213 | 214 | @upyun_readdir = (url, cb)=> 215 | @upyun_api 216 | method: "GET" 217 | url: url 218 | , (e, data)=> 219 | return cb e if e 220 | files = data.split('\n').map (line)-> 221 | line = line.split '\t' 222 | return null unless line.length == 4 223 | return ( 224 | filename: line[0] 225 | url: url + encodeURIComponent line[0] 226 | isDirectory: line[1]=='F' 227 | length: Number line[2] 228 | mtime: 1000 * Number line[3] 229 | ) 230 | cb null, files.filter (file)-> file? 231 | @upyun_find_abort = -> 232 | @upyun_find = (url, cb)=> 233 | results = [] 234 | @upyun_find_abort = @upyun_readdir url, (e, files)=> 235 | return cb e if e 236 | async.eachSeries files, (file, doneEach)=> 237 | if file.isDirectory 238 | @upyun_find file.url + '/', (e, tmp)=> 239 | return doneEach e if e 240 | results.push item for item in tmp 241 | results.push file 242 | doneEach null 243 | else 244 | results.push file 245 | _.defer => doneEach null 246 | , (e)=> 247 | @upyun_find_abort = -> 248 | cb e, results 249 | @upyun_upload = (url, files, onProgress, cb)=> 250 | aborted = false 251 | api_aborting = null 252 | aborting = => 253 | aborted = true 254 | api_aborting?() 255 | status = 256 | total_files: files.length 257 | total_bytes: files.reduce ((a, b)-> a + b.length), 0 258 | current_files: 0 259 | current_bytes: 0 260 | 261 | async.eachSeries files, (file, doneEach)=> 262 | Fs.stat file.path, (e, stats)=> 263 | return doneEach (new Error '操作已取消') if aborted 264 | return doneEach e if e 265 | api_aborting = @upyun_api 266 | pipeRequest: file.path 267 | method: "PUT" 268 | headers: 269 | 'mkdir': 'true', 270 | length: stats.size 271 | url: url + file.url 272 | onRequestData: (data)=> 273 | status.current_bytes+= data.length 274 | onProgress status 275 | , (e)=> 276 | status.current_files+= 1 277 | onProgress status 278 | doneEach e 279 | , (e)=> 280 | cb e 281 | 282 | return aborting 283 | @action_downloadFolder = (filename, url)=> 284 | unless filename || filename = url.match(/([^\/]+)\/$/)?[1] 285 | filename = @bucket 286 | 287 | @shortOperation "正在列出目录 #{filename} 下的所有文件", (doneFind, $btnCancelFind)=> 288 | $btnCancelFind.show().click => @upyun_find_abort() 289 | @upyun_find url, (e, files)=> 290 | doneFind e 291 | unless e 292 | SelectFolder (savepath)=> 293 | @taskOperation "正在下载目录 #{filename} ...", (progressTransfer, doneTransfer, $btnCancelTransfer)=> 294 | total_files = files.length 295 | total_bytes = files.reduce ((a, b)-> a + b.length), 0 296 | current_files = 0 297 | current_bytes = 0 298 | aborting = null 299 | $btnCancelTransfer.click => 300 | aborting() if aborting? 301 | async.eachSeries files, ((file, doneEach)=> 302 | return (_.defer => doneEach null) if file.isDirectory 303 | segs = file.url.substring(url.length).split '/' 304 | segs = segs.map decodeURIComponent 305 | destpath = Path.join savepath, filename, Path.join.apply Path, segs 306 | 307 | Mkdirp Path.dirname(destpath), (e)=> 308 | return doneEach e if e 309 | _.defer => 310 | bytesWritten = 0 311 | current_files+= 1 312 | aborting = @upyun_api 313 | method: 'GET' 314 | url: file.url 315 | pipeResponse: destpath 316 | onData: (data)=> 317 | bytesWritten += data.length 318 | progressTransfer (Math.floor 100 * (current_bytes + bytesWritten) / total_bytes), "已下载:#{current_files} / #{total_files} (#{@humanFileSize current_bytes + bytesWritten} / #{@humanFileSize total_bytes})" 319 | , (e)=> 320 | current_bytes+= file.length unless e 321 | doneEach e 322 | 323 | ), (e)=> 324 | aborting = null 325 | doneTransfer e 326 | unless e 327 | msg = Messenger().post 328 | message: "目录 #{filename} 下载完毕" 329 | actions: 330 | ok: 331 | label: '确定' 332 | action: => 333 | msg.hide() 334 | open: 335 | label: "打开" 336 | action: => 337 | msg.hide() 338 | Electron.shell.openItem savepath 339 | @action_uploadFile = (filepath, filename, destpath)=> 340 | @taskOperation "正在上传 #{filename}", (progressTransfer, doneTransfer, $btnCancelTransfer)=> 341 | files = [] 342 | loadfileSync = (file)=> 343 | stat = Fs.statSync(file.path) 344 | if stat.isFile() 345 | file.length = stat.size 346 | files.push file 347 | if stat.isDirectory() 348 | for filename in Fs.readdirSync file.path 349 | loadfileSync 350 | path: Path.join(file.path, filename) 351 | url: file.url + '/' + encodeURIComponent filename 352 | try 353 | loadfileSync path: filepath, url: '' 354 | catch e 355 | return doneTransfer e if e 356 | $btnCancelTransfer.show().click @upyun_upload destpath, files, (status)=> 357 | progressTransfer (Math.floor 100 * status.current_bytes / status.total_bytes), "已上传:#{status.current_files} / #{status.total_files} (#{@humanFileSize status.current_bytes} / #{@humanFileSize status.total_bytes})" 358 | , (e)=> 359 | doneTransfer e 360 | unless e 361 | @m_changed_path = destpath.replace /[^\/]+$/, '' 362 | msg = Messenger().post 363 | message: "文件 #{filename} 上传完毕" 364 | actions: 365 | ok: 366 | label: '确定' 367 | action: => 368 | msg.hide() 369 | @action_show_url = (title, url)=> 370 | msg = Messenger().post 371 | message: "#{title}
#{url}
" 372 | actions: 373 | ok: 374 | label: '确定' 375 | action: => 376 | msg.hide() 377 | copy: 378 | label: '将该地址复制到剪切版' 379 | action: (ev)=> 380 | $(ev.currentTarget).text '已复制到剪切版' 381 | Electron.clipboard.writeText url 382 | @action_share = (url)=> 383 | url = "upyun://#{@username}:#{@password}@#{@bucket}#{url}" 384 | msg = Messenger().post 385 | message: """ 386 | 您可以通过以下地址向其他人分享该目录: 387 |
#{url}
388 | 注意:
    389 |
  1. 该地址中包含了当前操作员的授权信息,向他人分享该地址的同时,也同时分享了该操作员的身份。
  2. 390 |
  3. 当他人安装了“又拍云管理器时”后,便可以直接点击该链接以打开。
  4. 391 |
392 | """ 393 | actions: 394 | ok: 395 | label: '确定' 396 | action: => 397 | msg.hide() 398 | copy: 399 | label: '将该地址复制到剪切版' 400 | action: (ev)=> 401 | $(ev.currentTarget).text '已复制到剪切版' 402 | Electron.clipboard.writeText url, 'text' 403 | 404 | @jump_login = => 405 | @m_path = '/' 406 | @m_active = false 407 | @refresh_favs() 408 | $ '#filelist, #editor' 409 | .hide() 410 | $ '#login' 411 | .fadeIn() 412 | 413 | @jump_filelist = => 414 | @jump_path '/' 415 | @jump_path = (path)=> 416 | @m_path = path 417 | @m_changed_path = path 418 | @m_active = true 419 | @m_files = null 420 | $ '#filelist .preloader' 421 | .css 422 | opacity: 1 423 | $ '#inputFilter' 424 | .val '' 425 | $ '#login, #editor' 426 | .hide() 427 | $ '#filelist' 428 | .fadeIn() 429 | segs = $.makeArray(@m_path.match /\/[^\/]+/g).map (match)-> String(match).replace /^\//, '' 430 | segs = segs.map decodeURIComponent 431 | $ '#path' 432 | .empty() 433 | $ li = document.createElement 'li' 434 | .appendTo '#path' 435 | $ document.createElement 'a' 436 | .appendTo li 437 | .text @username 438 | .prepend @createIcon 'user' 439 | .attr 'href', '#' 440 | .click (ev)=> 441 | ev.preventDefault() 442 | @jump_login() 443 | $ li = document.createElement 'li' 444 | .toggleClass 'active', !segs.length 445 | .appendTo '#path' 446 | $ document.createElement 'a' 447 | .appendTo li 448 | .text "#{@bucket} (#{@origin})" 449 | .prepend @createIcon 'cloud' 450 | .attr 'href', "#{@origin}/" 451 | .data 'url', '/' 452 | for seg, i in segs 453 | url = '/' + segs[0..i].map(encodeURIComponent).join('/') + '/' 454 | $ li = document.createElement 'li' 455 | .toggleClass 'active', i == segs.length - 1 456 | .appendTo '#path' 457 | $ document.createElement 'a' 458 | .appendTo li 459 | .text seg 460 | .prepend @createIcon 'folder' 461 | .attr 'href', "#{@origin}#{url}" 462 | .data 'url', url 463 | $ '#path li:not(:first-child)>a' 464 | .click (ev)=> 465 | ev.preventDefault() 466 | @jump_path $(ev.currentTarget).data 'url' 467 | 468 | @refresh_filelist = (cb)=> 469 | cur_path = @m_path 470 | @upyun_readdir cur_path, (e, files)=> 471 | return cb e if e 472 | if @m_path == cur_path && JSON.stringify(@m_files) != JSON.stringify(files) 473 | $('#filelist tbody').empty() 474 | $('#filelist .preloader').css 475 | opacity: 0 476 | for file in @m_files = files 477 | $ tr = document.createElement 'tr' 478 | .appendTo '#filelist tbody' 479 | $ td = document.createElement 'td' 480 | .appendTo tr 481 | if file.isDirectory 482 | $ a = document.createElement 'a' 483 | .appendTo td 484 | .text file.filename 485 | .prepend @createIcon 'folder' 486 | .attr 'href', "#" 487 | .data 'url', file.url + '/' 488 | .click (ev)=> 489 | ev.preventDefault() 490 | @jump_path $(ev.currentTarget).data('url') 491 | else 492 | $ td 493 | .text file.filename 494 | .prepend @createIcon 'file' 495 | $ document.createElement 'td' 496 | .appendTo tr 497 | .text if file.isDirectory then '' else @humanFileSize file.length 498 | $ document.createElement 'td' 499 | .appendTo tr 500 | .text moment(file.mtime).format 'LLL' 501 | $ td = document.createElement 'td' 502 | .appendTo tr 503 | if file.isDirectory 504 | $ document.createElement 'button' 505 | .appendTo td 506 | .attr title: '删除该目录' 507 | .addClass 'btn btn-danger btn-xs' 508 | .data 'url', file.url + '/' 509 | .data 'filename', file.filename 510 | .prepend @createIcon 'trash-o' 511 | .click (ev)=> 512 | filename = $(ev.currentTarget).data 'filename' 513 | url = $(ev.currentTarget).data 'url' 514 | @shortOperation "正在列出目录 #{filename} 下的所有文件", (doneFind, $btnCancelFind)=> 515 | $btnCancelFind.show().click => @upyun_find_abort() 516 | @upyun_find url, (e, files)=> 517 | doneFind e 518 | unless e 519 | files_deleting = 0 520 | async.eachSeries files, (file, doneEach)=> 521 | files_deleting+= 1 522 | @shortOperation "正在删除(#{files_deleting}/#{files.length}) #{file.filename}", (operationDone, $btnCancelDel)=> 523 | $btnCancelDel.show().click @upyun_api 524 | method: "DELETE" 525 | url: file.url 526 | , (e)=> 527 | operationDone e 528 | doneEach e 529 | , (e)=> 530 | unless e 531 | @shortOperation "正在删除 #{filename}", (operationDone, $btnCancelDel)=> 532 | $btnCancelDel.show().click @upyun_api 533 | method: "DELETE" 534 | url: url 535 | , (e)=> 536 | @m_changed_path = url.replace /[^\/]+\/$/, '' 537 | operationDone e 538 | else 539 | $ document.createElement 'button' 540 | .appendTo td 541 | .attr title: '删除该文件' 542 | .addClass 'btn btn-danger btn-xs' 543 | .data 'url', file.url 544 | .data 'filename', file.filename 545 | .prepend @createIcon 'trash-o' 546 | .click (ev)=> 547 | url = $(ev.currentTarget).data('url') 548 | filename = $(ev.currentTarget).data('filename') 549 | @shortOperation "正在删除 #{filename}", (operationDone, $btnCancelDel)=> 550 | $btnCancelDel.show().click @upyun_api 551 | method: "DELETE" 552 | url: url 553 | , (e)=> 554 | @m_changed_path = url.replace /\/[^\/]+$/, '/' 555 | operationDone e 556 | if file.isDirectory 557 | $ document.createElement 'button' 558 | .appendTo td 559 | .attr title: '下载该目录' 560 | .addClass 'btn btn-info btn-xs' 561 | .prepend @createIcon 'download' 562 | .data 'url', file.url + '/' 563 | .data 'filename', file.filename 564 | .click (ev)=> 565 | @action_downloadFolder $(ev.currentTarget).data('filename'), $(ev.currentTarget).data('url') 566 | $ document.createElement 'button' 567 | .appendTo td 568 | .attr title: '向其他人分享该目录' 569 | .addClass 'btn btn-info btn-xs' 570 | .prepend @createIcon 'share' 571 | .data 'url', file.url + '/' 572 | .click (ev)=> 573 | url = $(ev.currentTarget).data 'url' 574 | @action_share url 575 | else 576 | $ document.createElement 'button' 577 | .appendTo td 578 | .attr title: '下载该文件' 579 | .addClass 'btn btn-info btn-xs' 580 | .prepend @createIcon 'download' 581 | .data 'url', file.url 582 | .data 'filename', file.filename 583 | .data 'length', file.length 584 | .click (ev)=> 585 | filename = $(ev.currentTarget).data 'filename' 586 | url = $(ev.currentTarget).data 'url' 587 | length = $(ev.currentTarget).data 'length' 588 | SaveAsFile filename, (savepath)=> 589 | @taskOperation "正在下载文件 #{filename} ..", (progressTransfer, doneTransfer, $btnCancelTransfer)=> 590 | aborting = null 591 | $btnCancelTransfer.click => 592 | aborting() if aborting? 593 | _.defer => 594 | bytesWritten = 0 595 | aborting = @upyun_api 596 | method: "GET" 597 | url: url 598 | pipeResponse: savepath 599 | onData: (data)=> 600 | bytesWritten += data.length 601 | progressTransfer (Math.floor 100 * bytesWritten / length), "#{@humanFileSize bytesWritten} / #{@humanFileSize length}" 602 | , (e, data)=> 603 | doneTransfer e 604 | unless e 605 | msg = Messenger().post 606 | message: "文件 #{filename} 下载完毕" 607 | actions: 608 | ok: 609 | label: '确定' 610 | action: => 611 | msg.hide() 612 | open: 613 | label: "打开" 614 | action: => 615 | msg.hide() 616 | Electron.shell.openItem savepath 617 | showItemInFolder: 618 | label: "打开目录" 619 | action: => 620 | msg.hide() 621 | Electron.shell.showItemInFolder savepath 622 | $ document.createElement 'button' 623 | .appendTo td 624 | .attr title: '在浏览器中访问该文件' 625 | .addClass 'btn btn-info btn-xs' 626 | .prepend @createIcon 'globe' 627 | .data 'url', "#{@origin}#{file.url}" 628 | .click (ev)=> 629 | url = $(ev.currentTarget).data 'url' 630 | Electron.shell.openExternal url 631 | $ document.createElement 'button' 632 | .appendTo td 633 | .attr title: '公共地址' 634 | .addClass 'btn btn-info btn-xs' 635 | .prepend @createIcon 'code' 636 | .data 'url', "#{@origin}#{file.url}" 637 | .data 'filename', file.filename 638 | .click (ev)=> 639 | filename = $(ev.currentTarget).data 'filename' 640 | url = $(ev.currentTarget).data 'url' 641 | @action_show_url "文件 #{filename} 的公共地址(URL)", url 642 | $ document.createElement 'button' 643 | .appendTo td 644 | .attr title: '用文本编辑器打开该文件' 645 | .addClass 'btn btn-info btn-xs' 646 | .prepend @createIcon 'edit' 647 | .data 'url', file.url 648 | .data 'filename', file.filename 649 | .click (ev)=> 650 | Open 'editor', 651 | username: @username 652 | password: @password 653 | bucket: @bucket 654 | editor_url: $(ev.currentTarget).data 'url' 655 | editor_filename: $(ev.currentTarget).data 'filename' 656 | $('#filelist tbody [title]') 657 | .tooltip 658 | placement: 'bottom' 659 | trigger: 'hover' 660 | cb null 661 | @jump_editor = => 662 | $ '#login, #filelist' 663 | .hide() 664 | $ '#editor' 665 | .show() 666 | window.document.title = @editor_filename 667 | @editor = Ace.edit $('#editor .editor')[0] 668 | $('#btnReloadEditor').click() 669 | 670 | 671 | window.ondragover = window.ondrop = (ev)-> 672 | ev.preventDefault() 673 | return false 674 | $ => 675 | @messengerTasks = $ document.createElement 'ul' 676 | .appendTo 'body' 677 | .messenger() 678 | forverCounter = 0 679 | async.forever (doneForever)=> 680 | if @m_active && !@shortOperationBusy 681 | forverCounter += 1 682 | if forverCounter == 20 || @m_changed_path == @m_path 683 | forverCounter = 0 684 | @m_changed_path = null 685 | return @refresh_filelist (e)=> 686 | if e 687 | msg = Messenger().post 688 | message: e.message 689 | type: 'error' 690 | actions: 691 | ok: 692 | label: '确定' 693 | action: => 694 | msg.hide() 695 | @jump_login() 696 | setTimeout (=>doneForever null), 100 697 | setTimeout (=>doneForever null), 100 698 | , (e)=> 699 | throw e 700 | 701 | 702 | $ '#inputBucket' 703 | .on 'input', (event)=> 704 | origin = $('#inputOrigin').val() 705 | return unless origin.endsWith('.b0.upaiyun.com') 706 | $('#inputOrigin').val("http://#{event.currentTarget.value}.b0.upaiyun.com") 707 | $ '#inputOrigin' 708 | .on 'change', (event)=> 709 | if !event.currentTarget.value.trim() 710 | $(event.currentTarget).val("http://#{$('#inputBucket').val()}.b0.upaiyun.com") 711 | $ '#btnAddFav' 712 | .click => 713 | fav = $('#formLogin').serializeObject() 714 | fav.password = "MD5_" + MD5 fav.password unless fav.password.match /^MD5_/ 715 | @m_favs["#{fav.username}@#{fav.bucket}"] = fav 716 | localStorage.favs = JSON.stringify @m_favs 717 | @refresh_favs() 718 | $ '#formLogin' 719 | .submit (ev)=> 720 | ev.preventDefault() 721 | @[k] = v for k, v of $(ev.currentTarget).serializeObject() 722 | @password = "MD5_" + MD5 @password unless @password.match /^MD5_/ 723 | $ '#filelist tbody' 724 | .empty() 725 | @jump_filelist() 726 | $ window 727 | .on 'dragover', -> $('body').addClass 'drag_hover' 728 | .on 'dragleave', -> $('body').removeClass 'drag_hover' 729 | .on 'dragend', -> $('body').removeClass 'drag_hover' 730 | .on 'drop', (ev)=> 731 | $('body').removeClass 'drag_hover' 732 | ev.preventDefault() 733 | for file in ev.originalEvent.dataTransfer.files 734 | @action_uploadFile file.path, file.name, "#{@m_path}#{encodeURIComponent file.name}" 735 | $ '#inputFilter' 736 | .keydown => 737 | _.defer => 738 | val = String $('#inputFilter').val() 739 | $ "#filelist tbody tr:contains(#{JSON.stringify val})" 740 | .removeClass 'filtered' 741 | $ "#filelist tbody tr:not(:contains(#{JSON.stringify val}))" 742 | .addClass 'filtered' 743 | $ '#btnDownloadFolder' 744 | .click (ev)=> 745 | ev.preventDefault() 746 | @action_downloadFolder null, @m_path 747 | $ '#btnUploadFiles' 748 | .click (ev)=> 749 | ev.preventDefault() 750 | SelectFiles (files)=> 751 | for filepath in files 752 | filename = Path.basename filepath 753 | @action_uploadFile filepath, filename, "#{@m_path}#{encodeURIComponent filename}" 754 | $ '#btnUploadFolder' 755 | .click (ev)=> 756 | ev.preventDefault() 757 | SelectFolder (dirpath)=> 758 | @action_uploadFile dirpath, Path.basename(dirpath), @m_path 759 | $ '#btnCreateFolder' 760 | .click (ev)=> 761 | ev.preventDefault() 762 | Prompt "请输入新目录的名称", (filename)=> 763 | @shortOperation "正在新建目录 #{filename} ...", (doneCreating, $btnCancelCreateing)=> 764 | cur_path = @m_path 765 | $btnCancelCreateing.click @upyun_api 766 | url: "#{cur_path}#{filename}" 767 | method: "POST" 768 | headers: 769 | 'Folder': 'true' 770 | , (e, data)=> 771 | doneCreating e 772 | @m_changed_path = cur_path 773 | $ '#btnCreateFile' 774 | .click (ev)=> 775 | ev.preventDefault() 776 | Prompt "请输入新文件的文件名", (filename)=> 777 | Open 'editor', 778 | username: @username 779 | password: @password 780 | bucket: @bucket 781 | editor_url: "#{@m_path}#{filename}" 782 | editor_filename: filename 783 | $ '#btnReloadEditor' 784 | .click (ev)=> 785 | ev.preventDefault() 786 | @shortOperation "正在加载文件 #{@editor_filename} ...", (doneReloading, $btnCancelReloading)=> 787 | $btnCancelReloading.click @upyun_api 788 | url: @editor_url 789 | method: 'GET' 790 | , (e, data)=> 791 | if e 792 | data = '' 793 | else 794 | data = data.toString 'utf8' 795 | doneReloading null 796 | unless e 797 | @editor.setValue data 798 | $ '#btnShareFolder' 799 | .click (ev)=> 800 | @action_share @m_path 801 | $ '#btnSaveEditor' 802 | .click (ev)=> 803 | ev.preventDefault() 804 | @shortOperation "正在保存文件 #{@editor_filename} ...", (doneSaving, $btnCancelSaving)=> 805 | $btnCancelSaving.click @upyun_api 806 | url: @editor_url 807 | method: 'PUT' 808 | data: new Buffer @editor.getValue(), 'utf8' 809 | , (e)=> 810 | doneSaving e 811 | unless e 812 | msg = Messenger().post 813 | message: "成功保存文件 #{@editor_filename}" 814 | actions: 815 | ok: 816 | label: '确定' 817 | action: => 818 | msg.hide() 819 | $ '#btnLogout' 820 | .click (ev)=> 821 | ev.preventDefault() 822 | @jump_login() 823 | $ '#btnIssues' 824 | .click (ev)=> 825 | ev.preventDefault() 826 | Electron.shell.openExternal "https://github.com/layerssss/manager-for-upyun/issues" 827 | $ '[title]' 828 | .tooltip 829 | placement: 'bottom' 830 | trigger: 'hover' 831 | 832 | window.Init = (action, params) => 833 | for key, value of params 834 | @[key] = value 835 | @["jump_#{action}"]() 836 | 837 | 838 | -------------------------------------------------------------------------------- /source/javascripts/api.js: -------------------------------------------------------------------------------- 1 | window.Electron = require('electron'); 2 | 3 | const UUID = require('uuid'); 4 | 5 | const requestEventHandlers = {}; 6 | 7 | window.Mkdirp = (path, cb) => { 8 | const id = UUID.v4(); 9 | 10 | requestEventHandlers[id] = { 11 | callback: (error) => { 12 | requestEventHandlers[id] = null; 13 | cb(error); 14 | } 15 | } 16 | 17 | Electron.ipcRenderer.send('api', { 18 | id, 19 | apiName: 'mkdirp', 20 | apiOptions: { 21 | path 22 | } 23 | }); 24 | }; 25 | 26 | window.CallRequest = ({ 27 | options, 28 | onData, 29 | onRequestData, 30 | onFinish, 31 | pipeRequest, 32 | pipeResponse 33 | }) => { 34 | const id = UUID.v4(); 35 | 36 | requestEventHandlers[id] = { 37 | onData, 38 | onRequestData, 39 | onFinish: (...args) => { 40 | requestEventHandlers[id] = null; 41 | onFinish(...args); 42 | } 43 | }; 44 | 45 | Electron.ipcRenderer.send('api', { 46 | id, 47 | apiName: 'request', 48 | apiOptions: { 49 | options, 50 | pipeRequest, 51 | pipeResponse 52 | } 53 | }); 54 | 55 | return { 56 | abort: () => { 57 | Electron.ipcRenderer.send('api', { 58 | apiName: 'request-abort', 59 | id: id 60 | }); 61 | } 62 | }; 63 | }; 64 | 65 | Electron.ipcRenderer.on('api-event', (evt, { 66 | id, 67 | eventName, 68 | args 69 | }) => { 70 | if (!requestEventHandlers[id] || !requestEventHandlers[id][eventName]) return; 71 | requestEventHandlers[id][eventName](...args); 72 | }); 73 | 74 | window.SelectFiles = (cb) => { 75 | Electron.remote.dialog.showOpenDialog( 76 | Electron.remote.getCurrentWindow(), { 77 | properties: ['multiSelections', 'openFile', 'treatPackageAsDirectory'] 78 | }, 79 | cb 80 | ); 81 | }; 82 | 83 | window.SelectFolder = (cb) => { 84 | Electron.remote.dialog.showOpenDialog( 85 | Electron.remote.getCurrentWindow(), { 86 | properties: ['openDirectory', 'treatPackageAsDirectory'] 87 | }, 88 | filePaths => { 89 | if (!filePaths || !filePaths[0]) return; 90 | cb(filePaths[0]); 91 | } 92 | ); 93 | }; 94 | 95 | window.SaveAsFile = (filename, cb) => { 96 | Electron.remote.dialog.showSaveDialog( 97 | Electron.remote.getCurrentWindow(), { 98 | defaultPath: filename 99 | }, 100 | cb 101 | ); 102 | }; 103 | 104 | window.Open = (action, params) => { 105 | const curWindow = Electron.remote.getCurrentWindow(); 106 | const newWindow = new Electron.remote.BrowserWindow({ 107 | parent: curWindow, 108 | x: curWindow.getPosition()[0] + 50, 109 | y: curWindow.getPosition()[1] + 50, 110 | }); 111 | newWindow.loadURL(location.href); 112 | 113 | newWindow.webContents.on('did-finish-load', () => { 114 | newWindow.webContents.send('init-params', { 115 | action, 116 | params 117 | }); 118 | }); 119 | }; 120 | 121 | window.Prompt = (title, cb) => { 122 | const id = UUID.v4(); 123 | 124 | requestEventHandlers[id] = { 125 | callback: (path) => { 126 | requestEventHandlers[id] = null; 127 | cb(path); 128 | } 129 | } 130 | 131 | Electron.ipcRenderer.send('api', { 132 | id, 133 | apiName: 'prompt', 134 | apiOptions: { 135 | title 136 | } 137 | }); 138 | }; 139 | 140 | window.Crypto = require('crypto'); 141 | window.Path = require('path'); 142 | window.Fs = require('fs'); 143 | window.Ace = require('exports-loader?ace!ace-builds/src-noconflict/ace.js'); 144 | 145 | Electron.ipcRenderer.on('init-params', (evt, { 146 | action, 147 | params 148 | }) => { 149 | Init(action, params); 150 | }); 151 | -------------------------------------------------------------------------------- /source/javascripts/index.js: -------------------------------------------------------------------------------- 1 | require('coffee-loader?sourceMap=true!./all.coffee'); 2 | require('./api.js'); 3 | -------------------------------------------------------------------------------- /source/javascripts/vendors.js: -------------------------------------------------------------------------------- 1 | 2 | window.jQuery = window.$ = require('jquery'); 3 | require('messenger/build/js/messenger.js'); 4 | window._ = require('underscore'); 5 | window.async = require('async'); 6 | require('bootstrap'); 7 | require('form-serializer'); 8 | window.MD5 = require('md5'); 9 | window._.mixin(require('underscore.string').exports()); 10 | window.moment = require('moment'); 11 | 12 | require('style-loader!css-loader!bootswatch/spacelab/bootstrap.css'); 13 | require('style-loader!css-loader!messenger/build/css/messenger.css'); 14 | require('style-loader!css-loader!messenger/build/css/messenger-spinner.css'); 15 | require('style-loader!css-loader!messenger/build/css/messenger-theme-future.css'); 16 | require('style-loader!css-loader!font-awesome/css/font-awesome.css'); 17 | 18 | require('style-loader!css-loader!sass-loader?sourceMap=true!../stylesheets/all.sass'); 19 | require('style-loader!css-loader!less-loader?sourceMap=true!../stylesheets/loaders.less'); 20 | -------------------------------------------------------------------------------- /source/stylesheets/all.sass: -------------------------------------------------------------------------------- 1 | body 2 | background-color: #fcfcfc 3 | -webkit-transition: background-color 4 | &.drag_hover 5 | background-color: #ccc 6 | &:not(.loading) 7 | > #loading, > #loadingText 8 | opacity: 0 9 | pointer-events: none 10 | &.loading 11 | pointer-events: none 12 | > * 13 | opacity: .3 14 | > * 15 | transition: opacity ease .5s 16 | .well 17 | padding: .3em 18 | a:hover 19 | text-decoration: none 20 | .progress-bar 21 | transition: none!important 22 | -webkit-transition: none!important 23 | #loading, #loadingText 24 | position: fixed 25 | left: 50% 26 | top: 50% 27 | opacity: 1 28 | #loading 29 | margin: -35px 0 0 -35px 30 | #loadingText 31 | pointer-events: all 32 | margin: -0.5em 0 0 40px 33 | 34 | #login 35 | position: fixed 36 | top: 0 37 | left: 0 38 | bottom: 0 39 | right: 0 40 | background-color: #eee 41 | overflow-x: hidden 42 | overflow-y: auto 43 | .login_container 44 | margin: 1em 10% 45 | #favs 46 | li 47 | position: relative 48 | button 49 | position: absolute 50 | right: .7em 51 | top: .7em 52 | 53 | #filelist 54 | padding: 3em 2em 0 2em 55 | .btn 56 | margin: 0 2px 57 | .preloader 58 | position: fixed 59 | top: 3em 60 | margin-left: -1.25em 61 | left: 50% 62 | transition: opacity ease .2s 63 | tbody 64 | tr 65 | > td, >td i 66 | transition: line-height ease .2s, opacity ease .2s, padding ease .2s 67 | &.filtered 68 | > td, >td i 69 | line-height: 0 70 | opacity: 0 71 | padding-top: 0 72 | padding-bottom: 0 73 | border-top: none 74 | .btn 75 | display: none 76 | #topbar 77 | position: fixed 78 | top: 0 79 | left: 0 80 | right: 0 81 | box-shadow: 0 0 20px black 82 | > ol 83 | border-radius: 0 84 | 85 | #toolbar 86 | position: absolute 87 | right: 3px 88 | top: 4px 89 | #inputFilter 90 | display: inline 91 | transition: width ease .2s 92 | height: 28px 93 | width: 150px 94 | &:focus,&:hover 95 | width: 200px 96 | > * 97 | margin: 0 98 | #editor 99 | .navbar 100 | border-top: none 101 | border-left: none 102 | border-right: none 103 | border-radius: 0 104 | .editor 105 | position: absolute 106 | top: 50px 107 | left: 0px 108 | right: 0px 109 | bottom: 0px 110 | -------------------------------------------------------------------------------- /source/stylesheets/loaders.less: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | 3 | // Copyright (c) 2014 Luke Haas 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | // this software and associated documentation files (the "Software"), to deal in 7 | // the Software without restriction, including without limitation the rights to 8 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | // the Software, and to permit persons to whom the Software is furnished to do so, 10 | // 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, FITNESS 17 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | .load6 .loader { 23 | font-size:50px; 24 | text-indent:-9999em; 25 | overflow: hidden; 26 | width:1em; 27 | height:1em; 28 | border-radius:50%; 29 | margin:0.8em auto; 30 | position:relative; 31 | -webkit-animation:load6 1.7s infinite ease; 32 | animation:load6 1.7s infinite ease; 33 | } 34 | @-webkit-keyframes load6 {.load6-frames;} 35 | @keyframes load6 {.load6-frames;} 36 | 37 | .load6-frames() { 38 | 0% { 39 | -webkit-transform:rotate(0deg); 40 | transform:rotate(0deg); 41 | box-shadow:-0.11em -0.83em 0 -0.4em #555, 42 | -0.11em -0.83em 0 -0.42em #555, 43 | -0.11em -0.83em 0 -0.44em #555, 44 | -0.11em -0.83em 0 -0.46em #555, 45 | -0.11em -0.83em 0 -0.477em #555; 46 | } 47 | 5%,95% { 48 | box-shadow:-0.11em -0.83em 0 -0.4em #555, 49 | -0.11em -0.83em 0 -0.42em #555, 50 | -0.11em -0.83em 0 -0.44em #555, 51 | -0.11em -0.83em 0 -0.46em #555, 52 | -0.11em -0.83em 0 -0.477em #555; 53 | } 54 | 55 | 30% { 56 | box-shadow:-0.11em -0.83em 0 -0.4em #555, 57 | -0.51em -0.66em 0 -0.42em #555, 58 | -0.75em -0.36em 0 -0.44em #555, 59 | -0.83em -0.03em 0 -0.46em #555, 60 | -0.81em 0.21em 0 -0.477em #555; 61 | } 62 | 55% { 63 | box-shadow:-0.11em -0.83em 0 -0.4em #555, 64 | -0.29em -0.78em 0 -0.42em #555, 65 | -0.43em -0.72em 0 -0.44em #555, 66 | -0.52em -0.65em 0 -0.46em #555, 67 | -0.57em -0.61em 0 -0.477em #555; 68 | } 69 | 100% { 70 | -webkit-transform:rotate(360deg); 71 | transform:rotate(360deg); 72 | box-shadow:-0.11em -0.83em 0 -0.4em #555, 73 | -0.11em -0.83em 0 -0.42em #555, 74 | -0.11em -0.83em 0 -0.44em #555, 75 | -0.11em -0.83em 0 -0.46em #555, 76 | -0.11em -0.83em 0 -0.477em #555; 77 | } 78 | } 79 | 80 | 81 | 82 | .load7 .loader:before,.load7 .loader:after,.load7 .loader { 83 | border-radius:50%; 84 | width:2.5em; 85 | height:2.5em; 86 | -webkit-animation-fill-mode: both; 87 | animation-fill-mode: both; 88 | -webkit-animation:load7 1.8s infinite ease-in-out; 89 | animation:load7 1.8s infinite ease-in-out; 90 | } 91 | .load7 .loader { 92 | margin:8em auto; 93 | font-size:10px; 94 | position:relative; 95 | text-indent:-9999em; 96 | -webkit-animation-delay:-0.16s; 97 | animation-delay:-0.16s; 98 | } 99 | .load7 .loader:before { 100 | left:-3.5em; 101 | -webkit-animation-delay:-0.32s; 102 | animation-delay:-0.32s; 103 | } 104 | .load7 .loader:after { 105 | left:3.5em; 106 | } 107 | .load7 .loader:before,.loader:after { 108 | content:''; 109 | position:absolute; 110 | top:0; 111 | } 112 | 113 | @-webkit-keyframes load7 {.load7-frames;} 114 | @keyframes load7 {.load7-frames;} 115 | 116 | .load7-frames() { 117 | 0%,80%,100% { 118 | box-shadow:0 2.5em 0 -1.3em #555; 119 | } 120 | 40% { 121 | box-shadow:0 2.5em 0 0 #555; 122 | } 123 | } 124 | 125 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 2 | const Path = require('path'); 3 | 4 | module.exports = { 5 | entry: { 6 | 'vendors': "./source/javascripts/vendors.js", 7 | 'index': "./source/javascripts/index.js" 8 | }, 9 | target: 'electron', 10 | output: { 11 | path: Path.join(__dirname, 'bundle'), 12 | filename: "[name].js" 13 | }, 14 | module: { 15 | rules: [{ 16 | test: /\.(png|woff|woff2|eot|ttf|svg)$/, 17 | loader: 'url-loader' 18 | }] 19 | } 20 | }; 21 | --------------------------------------------------------------------------------