ipcam 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +56 -0
- data/COPYRIGHT.md +99 -0
- data/Gemfile +2 -0
- data/LICENSE +21 -0
- data/README.md +59 -0
- data/Rakefile +2 -0
- data/bin/ipcam +129 -0
- data/ipcam.gemspec +50 -0
- data/lib/ipcam/main.rb +344 -0
- data/lib/ipcam/version.rb +3 -0
- data/lib/ipcam/webserver.rb +178 -0
- data/lib/ipcam/websock.rb +380 -0
- data/resource/common/js/msgpack-rpc.js +191 -0
- data/resource/common/js/util.js +135 -0
- data/resource/extern/css/bootstrap.min.css +7 -0
- data/resource/extern/css/ion.rangeSlider.min.css +1 -0
- data/resource/extern/css/pretty-checkbox.min.css +12 -0
- data/resource/extern/js/bootstrap.min.js +7 -0
- data/resource/extern/js/ion.rangeSlider.min.js +2 -0
- data/resource/extern/js/jquery-3.4.1.min.js +2 -0
- data/resource/extern/js/jquery.nicescroll.min.js +2 -0
- data/resource/extern/js/msgpack.min.js +2 -0
- data/resource/extern/js/popper.min.js +5 -0
- data/resource/ipcam/js/main.js +486 -0
- data/resource/ipcam/js/session.js +61 -0
- data/resource/ipcam/scss/main/style.scss +91 -0
- data/resource/ipcam/views/main.erb +68 -0
- data/resource/ipcam/views/settings.erb +19 -0
- data/run.sh +3 -0
- metadata +228 -0
@@ -0,0 +1,380 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
#
|
5
|
+
# Sample for v4l2-ruby
|
6
|
+
#
|
7
|
+
# Copyright (C) 2019 Hiroshi Kuwagata <kgt9221@gmail.com>
|
8
|
+
#
|
9
|
+
|
10
|
+
require 'fileutils'
|
11
|
+
require 'em-websocket'
|
12
|
+
require 'msgpack/rpc/server'
|
13
|
+
|
14
|
+
module IPCam
|
15
|
+
class WebSocket
|
16
|
+
include MessagePack::Rpc::Server
|
17
|
+
|
18
|
+
msgpack_options :symbolize_keys => true
|
19
|
+
|
20
|
+
class << self
|
21
|
+
#
|
22
|
+
# セッションリストの取得
|
23
|
+
#
|
24
|
+
# @return [Array<WebSocket>] セッションリスト
|
25
|
+
#
|
26
|
+
def session_list
|
27
|
+
return @session_list ||= []
|
28
|
+
end
|
29
|
+
private :session_list
|
30
|
+
|
31
|
+
#
|
32
|
+
# クリティカルセクションの設置
|
33
|
+
#
|
34
|
+
# @yield クリティカルセクションとして処理するブロック
|
35
|
+
#
|
36
|
+
# @return [Object] ブロックの戻り値
|
37
|
+
#
|
38
|
+
def sync(&proc)
|
39
|
+
return (@mutex ||= Mutex.new).synchronize(&proc)
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# セッションリストへのセッション追加
|
44
|
+
#
|
45
|
+
# @param [Socket] sock セッションリストに追加するソケットオブジェクト
|
46
|
+
#
|
47
|
+
# @return [WebSocket]
|
48
|
+
# ソケットオブジェクトに紐付けられたセッションオブジェクト
|
49
|
+
#
|
50
|
+
# @note
|
51
|
+
# 受け取ったソケットオブジェクトを元に、セッションオブジェクトを
|
52
|
+
# 生成し、そのオブジェクトをセッションリストに追加する
|
53
|
+
#
|
54
|
+
def join(sock)
|
55
|
+
sync {
|
56
|
+
if session_list.any? {|s| s === sock}
|
57
|
+
raise("Already joined #{sock}")
|
58
|
+
end
|
59
|
+
|
60
|
+
ret = self.new(@app, sock)
|
61
|
+
session_list << ret
|
62
|
+
|
63
|
+
return ret
|
64
|
+
}
|
65
|
+
end
|
66
|
+
private :join
|
67
|
+
|
68
|
+
#
|
69
|
+
# セッションオブジェクトからのセッション削除
|
70
|
+
#
|
71
|
+
# @param [Socket] sock
|
72
|
+
# セッションオブジェクトを特定するためのソケットオブジェクト
|
73
|
+
#
|
74
|
+
def bye(sock)
|
75
|
+
sync {
|
76
|
+
session_list.reject! { |s|
|
77
|
+
if s === sock
|
78
|
+
s.finish
|
79
|
+
true
|
80
|
+
else
|
81
|
+
false
|
82
|
+
end
|
83
|
+
}
|
84
|
+
}
|
85
|
+
end
|
86
|
+
|
87
|
+
#
|
88
|
+
# イベント情報の一斉送信
|
89
|
+
#
|
90
|
+
# @param [String] name イベント名
|
91
|
+
# @param [Array] args イベントで通知する引数
|
92
|
+
#
|
93
|
+
def broadcast(name, *args)
|
94
|
+
sync {session_list.each {|s| s.notify(name, *args)}}
|
95
|
+
end
|
96
|
+
|
97
|
+
#
|
98
|
+
# バインド先のURL文字列を生成する
|
99
|
+
#
|
100
|
+
# @return [String] URL文字列
|
101
|
+
#
|
102
|
+
def bind_url
|
103
|
+
if $bind_addr.include?(":")
|
104
|
+
addr = "[#{$bind_addr}]" if $bind_addr.include?(":")
|
105
|
+
else
|
106
|
+
addr = $bind_addr
|
107
|
+
end
|
108
|
+
|
109
|
+
return "tcp://#{addr}:#{$http_port}"
|
110
|
+
end
|
111
|
+
private :bind_url
|
112
|
+
|
113
|
+
#
|
114
|
+
# WebSocket制御の開始
|
115
|
+
#
|
116
|
+
def start(app)
|
117
|
+
EM.defer {
|
118
|
+
@app = app
|
119
|
+
|
120
|
+
sleep 1 until EM.reactor_running?
|
121
|
+
|
122
|
+
$logger.info("websock") {"started (#{bind_url()})"}
|
123
|
+
|
124
|
+
EM::WebSocket.start(:host => $bind_addr, :port => $ws_port) { |sock|
|
125
|
+
peer = Socket.unpack_sockaddr_in(sock.get_peername)
|
126
|
+
addr = peer[1]
|
127
|
+
port = peer[0]
|
128
|
+
serv = join(sock)
|
129
|
+
|
130
|
+
sock.set_sock_opt(Socket::Constants::SOL_SOCKET,
|
131
|
+
Socket::SO_KEEPALIVE,
|
132
|
+
true)
|
133
|
+
|
134
|
+
sock.set_sock_opt(Socket::IPPROTO_TCP,
|
135
|
+
Socket::TCP_QUICKACK,
|
136
|
+
true)
|
137
|
+
|
138
|
+
sock.set_sock_opt(Socket::IPPROTO_TCP,
|
139
|
+
Socket::TCP_NODELAY,
|
140
|
+
false)
|
141
|
+
|
142
|
+
sock.onopen {
|
143
|
+
$logger.info("websock") {"connection from #{addr}:#{port}"}
|
144
|
+
}
|
145
|
+
|
146
|
+
sock.onbinary { |msg|
|
147
|
+
begin
|
148
|
+
serv.receive_dgram(msg)
|
149
|
+
|
150
|
+
rescue => e
|
151
|
+
$logger.error("websock") {
|
152
|
+
"error occured: #{e.message} (#{e.backtrace[0]})"
|
153
|
+
}
|
154
|
+
end
|
155
|
+
}
|
156
|
+
|
157
|
+
sock.onclose {
|
158
|
+
$logger.info("websock") {
|
159
|
+
"connection close from #{addr}:#{port}"
|
160
|
+
}
|
161
|
+
|
162
|
+
bye(sock)
|
163
|
+
}
|
164
|
+
}
|
165
|
+
}
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
#
|
170
|
+
# セッションオブジェクトのイニシャライザ
|
171
|
+
#
|
172
|
+
# @param [IPCam] app アプリケーション本体のインスタンス
|
173
|
+
# @param [Socket] sock Socketインスタンス
|
174
|
+
#
|
175
|
+
def initialize(app, sock)
|
176
|
+
@app = app
|
177
|
+
@sock = sock
|
178
|
+
@allow = []
|
179
|
+
|
180
|
+
peer = Socket.unpack_sockaddr_in(sock.get_peername)
|
181
|
+
@addr = peer[1]
|
182
|
+
@port = peer[0]
|
183
|
+
end
|
184
|
+
|
185
|
+
attr_reader :sock
|
186
|
+
|
187
|
+
#
|
188
|
+
# セッションオブジェクトの終了処理
|
189
|
+
#
|
190
|
+
def finish
|
191
|
+
end
|
192
|
+
|
193
|
+
#
|
194
|
+
# peerソケットへのデータ送信
|
195
|
+
#
|
196
|
+
# @param [String] data 送信するデータ
|
197
|
+
#
|
198
|
+
# @note MessagePack::Rpc::Serverのオーバーライド
|
199
|
+
#
|
200
|
+
def send_data(data)
|
201
|
+
@sock.send_binary(data)
|
202
|
+
end
|
203
|
+
private :send_data
|
204
|
+
|
205
|
+
#
|
206
|
+
# MessagePack-RPCのエラーハンドリング
|
207
|
+
#
|
208
|
+
# @param [StandardError] e 発生したエラーの例外オブジェクト
|
209
|
+
#
|
210
|
+
# @note MessagePack::Rpc::Serverのオーバーライド
|
211
|
+
#
|
212
|
+
def on_error(e)
|
213
|
+
$logger.error("websock") {e.message}
|
214
|
+
end
|
215
|
+
private :on_error
|
216
|
+
|
217
|
+
#
|
218
|
+
# 通知のブロードキャスト
|
219
|
+
#
|
220
|
+
# @param [String] name イベント名
|
221
|
+
# @param [Array] args イベントで通知する引数
|
222
|
+
#
|
223
|
+
def broadcast(name, *args)
|
224
|
+
self.class.broadcast(name, *arg)
|
225
|
+
end
|
226
|
+
private :broadcast
|
227
|
+
|
228
|
+
#
|
229
|
+
# 通知の送信
|
230
|
+
#
|
231
|
+
# @param [String] name イベント名
|
232
|
+
# @param [Array] args イベントで通知する引数
|
233
|
+
#
|
234
|
+
def notify(name, *args)
|
235
|
+
super(name, *args) if @allow == "*" or @allow.include?(name)
|
236
|
+
end
|
237
|
+
|
238
|
+
#
|
239
|
+
# 比較演算子の定義
|
240
|
+
#
|
241
|
+
def ===(obj)
|
242
|
+
return (self == obj || @sock == obj)
|
243
|
+
end
|
244
|
+
|
245
|
+
#
|
246
|
+
# RPC procedures
|
247
|
+
#
|
248
|
+
|
249
|
+
#
|
250
|
+
# 通知要求を設定する
|
251
|
+
#
|
252
|
+
# @param [Array] arg
|
253
|
+
#
|
254
|
+
# @return [:OK] 固定値
|
255
|
+
#
|
256
|
+
def add_notify_request(*args)
|
257
|
+
args.each {|type| @allow << type.to_sym}
|
258
|
+
args.uniq!
|
259
|
+
|
260
|
+
return :OK
|
261
|
+
end
|
262
|
+
remote_public :add_notify_request
|
263
|
+
|
264
|
+
#
|
265
|
+
# 通知要求をクリアする
|
266
|
+
#
|
267
|
+
# @param [Array] arg
|
268
|
+
#
|
269
|
+
# @return [:OK] 固定値
|
270
|
+
#
|
271
|
+
def clear_notify_request(*args)
|
272
|
+
args.each {|type| @allow.delete(type.to_sym)}
|
273
|
+
|
274
|
+
return :OK
|
275
|
+
end
|
276
|
+
remote_public :clear_notify_request
|
277
|
+
|
278
|
+
#
|
279
|
+
# 疎通確認用プロシジャー
|
280
|
+
#
|
281
|
+
# @return [:OK] 固定値
|
282
|
+
#
|
283
|
+
def hello
|
284
|
+
return :OK
|
285
|
+
end
|
286
|
+
remote_public :hello
|
287
|
+
|
288
|
+
#
|
289
|
+
# カメラ情報の取得
|
290
|
+
#
|
291
|
+
# @return [:OK] カメラ情報をパックしたハッシュ
|
292
|
+
#
|
293
|
+
def get_camera_info
|
294
|
+
return @app.get_camera_info()
|
295
|
+
end
|
296
|
+
remote_public :get_camera_info
|
297
|
+
|
298
|
+
#
|
299
|
+
# カメラ固有名の取得
|
300
|
+
#
|
301
|
+
# @return [:OK] カメラ情報をパックしたハッシュ
|
302
|
+
#
|
303
|
+
def get_ident_string
|
304
|
+
return @app.get_ident_string()
|
305
|
+
end
|
306
|
+
remote_public :get_ident_string
|
307
|
+
|
308
|
+
#
|
309
|
+
# カメラの設定情報の取得
|
310
|
+
#
|
311
|
+
# @return [Array] カメラの設定情報の配列
|
312
|
+
#
|
313
|
+
def get_config
|
314
|
+
return @app.get_config
|
315
|
+
end
|
316
|
+
remote_public :get_config
|
317
|
+
|
318
|
+
#
|
319
|
+
# 画像サイズの設定
|
320
|
+
#
|
321
|
+
# @param [Integer] width 新しい画像の幅
|
322
|
+
# @param [Integer] height 新しい画像の高さ
|
323
|
+
#
|
324
|
+
# @return [:OK] 固定値
|
325
|
+
#
|
326
|
+
# @note 画像サイズの変更に伴い、update_image_sizeイベントがブロード
|
327
|
+
# キャストされる
|
328
|
+
#
|
329
|
+
def set_image_size(width, height)
|
330
|
+
@app.set_image_size(width, height)
|
331
|
+
return :OK
|
332
|
+
end
|
333
|
+
remote_public :set_image_size
|
334
|
+
|
335
|
+
#
|
336
|
+
# フレームレートの設定
|
337
|
+
#
|
338
|
+
# @param [Integer] num 新しいフレームレートの値(分子)
|
339
|
+
# @param [Integer] deno 新しいフレームレートの値(分母)
|
340
|
+
#
|
341
|
+
# @return [:OK] 固定値
|
342
|
+
#
|
343
|
+
# @note 画像サイズの変更に伴い、update_framerateイベントがブロード
|
344
|
+
# キャストされる
|
345
|
+
#
|
346
|
+
def set_framerate(num, deno)
|
347
|
+
@app.set_framerate(num, deno)
|
348
|
+
return :OK
|
349
|
+
end
|
350
|
+
remote_public :set_framerate
|
351
|
+
|
352
|
+
#
|
353
|
+
# カメラの設定変更
|
354
|
+
#
|
355
|
+
# @param [Integer] id 設定項目のID
|
356
|
+
# @param [Integer] val 新しい設定項目の値
|
357
|
+
#
|
358
|
+
# @return [:OK] 固定値
|
359
|
+
#
|
360
|
+
# @note 画像サイズの変更に伴い、update_controlイベントがブロード
|
361
|
+
# キャストされる
|
362
|
+
#
|
363
|
+
def set_control(num, deno)
|
364
|
+
@app.set_control(num, deno)
|
365
|
+
return :OK
|
366
|
+
end
|
367
|
+
remote_public :set_control
|
368
|
+
|
369
|
+
#
|
370
|
+
# 設定値の保存
|
371
|
+
#
|
372
|
+
# @return [:OK] 固定値
|
373
|
+
#
|
374
|
+
def save_config
|
375
|
+
@app.save_config()
|
376
|
+
return :OK
|
377
|
+
end
|
378
|
+
remote_public :save_config
|
379
|
+
end
|
380
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
/*
|
2
|
+
* MessagePack RPC base class
|
3
|
+
*
|
4
|
+
* (C) 2017 Hiroshi Kuwagata <kgt9221@gmail.com>
|
5
|
+
*/
|
6
|
+
|
7
|
+
if (!msgpack) {
|
8
|
+
throw "msgpack-lite is not load yet"
|
9
|
+
}
|
10
|
+
|
11
|
+
(function () {
|
12
|
+
/* declar local symbol */
|
13
|
+
const newId = Symbol('newId');
|
14
|
+
const callback = Symbol('callback');
|
15
|
+
const recv = Symbol('recv');
|
16
|
+
|
17
|
+
/* declar contant */
|
18
|
+
const DEFAULT_URL = `ws://${location.hostname}:${parseInt(location.port)+1}/`;
|
19
|
+
const COEDEC = msgpack.createCodec({binarraybuffer:true, preset:true});
|
20
|
+
const ENC_OPT = {codec:COEDEC};
|
21
|
+
|
22
|
+
msgpack.rpc = class {
|
23
|
+
/*
|
24
|
+
* core functions
|
25
|
+
*/
|
26
|
+
constructor (url) {
|
27
|
+
this.url = url || DEFAULT_URL
|
28
|
+
this.sock = null;
|
29
|
+
this.deferred = null;
|
30
|
+
this.maxId = 1;
|
31
|
+
this.handlers = {}
|
32
|
+
}
|
33
|
+
|
34
|
+
[newId]() {
|
35
|
+
return this.maxId++;
|
36
|
+
}
|
37
|
+
|
38
|
+
[callback](name, args) {
|
39
|
+
if (this.handlers[name]) {
|
40
|
+
this.handlers[name](...args);
|
41
|
+
} else {
|
42
|
+
throw `unhandled notification received (${name}).`;
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
[recv](data) {
|
47
|
+
var self;
|
48
|
+
var msg;
|
49
|
+
var type;
|
50
|
+
var id;
|
51
|
+
var meth;
|
52
|
+
var err;
|
53
|
+
var res;
|
54
|
+
var para;
|
55
|
+
var $df;
|
56
|
+
|
57
|
+
msg = msgpack.decode(new Uint8Array(data), ENC_OPT);
|
58
|
+
type = msg[0];
|
59
|
+
|
60
|
+
switch (type) {
|
61
|
+
case 1:
|
62
|
+
id = msg[1];
|
63
|
+
err = msg[2];
|
64
|
+
res = msg[3];
|
65
|
+
$df = this.deferred[id];
|
66
|
+
|
67
|
+
if ($df) {
|
68
|
+
if (err) {
|
69
|
+
$df.reject(err);
|
70
|
+
} else {
|
71
|
+
$df.resolve(res);
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
delete this.deferred[id];
|
76
|
+
break;
|
77
|
+
|
78
|
+
case 2:
|
79
|
+
meth = msg[1];
|
80
|
+
para = msg[2];
|
81
|
+
|
82
|
+
if (!para) {
|
83
|
+
para = [];
|
84
|
+
} else if (!(para instanceof Array)) {
|
85
|
+
para = [para];
|
86
|
+
}
|
87
|
+
|
88
|
+
this[callback](meth, para);
|
89
|
+
break;
|
90
|
+
|
91
|
+
default:
|
92
|
+
throw `Illeagal data (type=${type}) recevied.`;
|
93
|
+
}
|
94
|
+
}
|
95
|
+
|
96
|
+
remoteCall(meth) {
|
97
|
+
var id;
|
98
|
+
var $df;
|
99
|
+
var args;
|
100
|
+
|
101
|
+
id = this[newId]();
|
102
|
+
$df = new $.Deferred();
|
103
|
+
args = Array.prototype.slice.call(arguments, 1);
|
104
|
+
|
105
|
+
switch (args.length) {
|
106
|
+
case 0:
|
107
|
+
args = null;
|
108
|
+
break;
|
109
|
+
|
110
|
+
case 1:
|
111
|
+
args = args[0];
|
112
|
+
break;
|
113
|
+
}
|
114
|
+
|
115
|
+
this.deferred[id] = $df;
|
116
|
+
this.sock.send(msgpack.encode([0, id, meth, args], ENC_OPT));
|
117
|
+
|
118
|
+
return $df.promise();
|
119
|
+
}
|
120
|
+
|
121
|
+
remoteNotify(meth) {
|
122
|
+
var args;
|
123
|
+
|
124
|
+
args = Array.prototype.slice.call(arguments, 1);
|
125
|
+
|
126
|
+
switch (args.length) {
|
127
|
+
case 0:
|
128
|
+
args = null;
|
129
|
+
break;
|
130
|
+
|
131
|
+
case 1:
|
132
|
+
args = args[0];
|
133
|
+
break;
|
134
|
+
}
|
135
|
+
|
136
|
+
this.sock.send(msgpack.encode([2, meth, args], ENC_OPT));
|
137
|
+
}
|
138
|
+
|
139
|
+
on(name, func) {
|
140
|
+
this.handlers[name] = func;
|
141
|
+
return this;
|
142
|
+
}
|
143
|
+
|
144
|
+
start() {
|
145
|
+
var $df;
|
146
|
+
|
147
|
+
$df = new $.Deferred();
|
148
|
+
|
149
|
+
if (!this.sock) {
|
150
|
+
this.sock = new WebSocket(this.url);
|
151
|
+
|
152
|
+
this.sock.binaryType = "arraybuffer";
|
153
|
+
|
154
|
+
this.sock.onopen = () => {
|
155
|
+
this.deferred = {}
|
156
|
+
$df.resolve();
|
157
|
+
};
|
158
|
+
|
159
|
+
this.sock.onerror = () => {
|
160
|
+
$df.reject();
|
161
|
+
};
|
162
|
+
|
163
|
+
this.sock.onmessage = (m) => {
|
164
|
+
this[recv](m.data);
|
165
|
+
};
|
166
|
+
|
167
|
+
this.sock.onclose = () => {
|
168
|
+
if (this.onSessionClosed) this.onSessionClosed();
|
169
|
+
this.sock = null;
|
170
|
+
};
|
171
|
+
}
|
172
|
+
|
173
|
+
return $df.promise();
|
174
|
+
}
|
175
|
+
|
176
|
+
finish() {
|
177
|
+
var id;
|
178
|
+
|
179
|
+
this.sock.close();
|
180
|
+
|
181
|
+
for (id in this.deferred) {
|
182
|
+
this.deferred[id].reject("session finished");
|
183
|
+
delete this.deferred[id];
|
184
|
+
}
|
185
|
+
|
186
|
+
this.sock = null;
|
187
|
+
this.deferred = null;
|
188
|
+
}
|
189
|
+
}
|
190
|
+
})();
|
191
|
+
|
@@ -0,0 +1,135 @@
|
|
1
|
+
/*
|
2
|
+
* 雑多な処理を集めたコード
|
3
|
+
*
|
4
|
+
* (C) 2017 Hiroshi Kuwagata <kgt9221@gmail.com>
|
5
|
+
*/
|
6
|
+
|
7
|
+
(function () {
|
8
|
+
function readJavaScript(src) {
|
9
|
+
return $.getScript(src);
|
10
|
+
}
|
11
|
+
|
12
|
+
function readCss(src) {
|
13
|
+
var $df;
|
14
|
+
|
15
|
+
$df = new $.Deferred();
|
16
|
+
|
17
|
+
$('head')
|
18
|
+
.append($('<link>')
|
19
|
+
.on('load', () => {
|
20
|
+
$df.resolve();
|
21
|
+
})
|
22
|
+
.attr("rel", "stylesheet")
|
23
|
+
.attr("type", "text/css")
|
24
|
+
.attr("href", src)
|
25
|
+
);
|
26
|
+
|
27
|
+
return $df.promise();
|
28
|
+
}
|
29
|
+
|
30
|
+
function getResource(list, $df) {
|
31
|
+
var src;
|
32
|
+
|
33
|
+
src = list.shift();
|
34
|
+
|
35
|
+
if (/\.js$/.test(src)) {
|
36
|
+
readJavaScript(src)
|
37
|
+
.then((script, state) => {
|
38
|
+
getResource(list, $df);
|
39
|
+
})
|
40
|
+
.fail((error) => {
|
41
|
+
$df.reject(error);
|
42
|
+
});
|
43
|
+
|
44
|
+
} else if (/\.(css|scss)$/.test(src)) {
|
45
|
+
readCss(src)
|
46
|
+
.then(() => {
|
47
|
+
getResource(list, $df);
|
48
|
+
})
|
49
|
+
.fail((error) => {
|
50
|
+
$df.reject(error);
|
51
|
+
});
|
52
|
+
|
53
|
+
} else if (src == null) {
|
54
|
+
$df.resolve();
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
function loadImage(url, dst) {
|
59
|
+
var $df;
|
60
|
+
|
61
|
+
$df = new $.Deferred();
|
62
|
+
|
63
|
+
if (!dst) {
|
64
|
+
dst = new Image();
|
65
|
+
} else {
|
66
|
+
if (dst instanceof jQuery) {
|
67
|
+
dst = dst[0];
|
68
|
+
}
|
69
|
+
|
70
|
+
if (!(dst instanceof Image)) {
|
71
|
+
throw("not image object");
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
$(dst)
|
76
|
+
.on('load', () => {
|
77
|
+
$df.resolve(dst);
|
78
|
+
})
|
79
|
+
.on('error', (e) => {
|
80
|
+
$df.reject(e);
|
81
|
+
})
|
82
|
+
.attr('src', url);
|
83
|
+
|
84
|
+
return $df.promise();
|
85
|
+
}
|
86
|
+
|
87
|
+
Utils = class {
|
88
|
+
static require(list) {
|
89
|
+
var $df;
|
90
|
+
|
91
|
+
$df = new $.Deferred()
|
92
|
+
|
93
|
+
getResource(list, $df);
|
94
|
+
|
95
|
+
return $df.promise();
|
96
|
+
}
|
97
|
+
|
98
|
+
static loadImageFromData(data) {
|
99
|
+
var $df;
|
100
|
+
var blob;
|
101
|
+
var url;
|
102
|
+
|
103
|
+
$df = new $.Deferred();
|
104
|
+
blob = new Blob([data.data], {type: data.type});
|
105
|
+
url = URL.createObjectURL(blob);
|
106
|
+
|
107
|
+
loadImage(url)
|
108
|
+
.then((img) => {
|
109
|
+
$df.resolve(img);
|
110
|
+
})
|
111
|
+
.fail((error) => {
|
112
|
+
$df.reject(error);
|
113
|
+
})
|
114
|
+
.always(() => {
|
115
|
+
URL.revokeObjectURL(url);
|
116
|
+
});
|
117
|
+
|
118
|
+
return $df.promise();
|
119
|
+
}
|
120
|
+
|
121
|
+
static copyToClipboard(text) {
|
122
|
+
var $text;
|
123
|
+
|
124
|
+
$text = $('<textarea>').css('visible', 'hidden');
|
125
|
+
$('body').append($text);
|
126
|
+
|
127
|
+
$text
|
128
|
+
.val(text)
|
129
|
+
.select();
|
130
|
+
document.execCommand('copy');
|
131
|
+
|
132
|
+
$text.remove();
|
133
|
+
}
|
134
|
+
}
|
135
|
+
})();
|