├── .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 | 
27 | 
28 | 
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 |
45 |
46 |
104 |
107 |
108 |
109 | |
110 | 尺寸 |
111 | 最后修改时间 |
112 | |
113 |
114 |
115 |
116 |
117 |
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 | - 该地址中包含了当前操作员的授权信息,向他人分享该地址的同时,也同时分享了该操作员的身份。
390 | - 当他人安装了“又拍云管理器时”后,便可以直接点击该链接以打开。
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 |
--------------------------------------------------------------------------------