narou 1.7.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of narou might be problematic. Click here for more details.

Files changed (100) hide show
  1. checksums.yaml +5 -13
  2. data/.gitignore +1 -0
  3. data/ChangeLog.md +35 -0
  4. data/Gemfile +3 -0
  5. data/LICENSE.txt +100 -0
  6. data/README.md +28 -39
  7. data/lib/color.rb +0 -2
  8. data/lib/command.rb +1 -0
  9. data/lib/command/convert.rb +33 -4
  10. data/lib/command/diff.rb +5 -4
  11. data/lib/command/download.rb +9 -1
  12. data/lib/command/flag.rb +2 -2
  13. data/lib/command/list.rb +1 -1
  14. data/lib/command/mail.rb +3 -3
  15. data/lib/command/remove.rb +2 -1
  16. data/lib/command/send.rb +7 -6
  17. data/lib/command/setting.rb +39 -95
  18. data/lib/command/tag.rb +25 -13
  19. data/lib/command/update.rb +6 -1
  20. data/lib/command/version.rb +5 -1
  21. data/lib/command/web.rb +111 -0
  22. data/lib/commandbase.rb +5 -2
  23. data/lib/commandline.rb +16 -0
  24. data/lib/converterbase.rb +20 -14
  25. data/lib/device.rb +5 -4
  26. data/lib/downloader.rb +68 -39
  27. data/lib/eventable.rb +72 -0
  28. data/lib/helper.rb +105 -37
  29. data/lib/ini.rb +2 -1
  30. data/lib/input.rb +68 -0
  31. data/lib/inventory.rb +4 -0
  32. data/lib/kindlestrip.rb +2 -2
  33. data/lib/logger.rb +41 -19
  34. data/lib/narou.rb +10 -0
  35. data/lib/narou/api.rb +1 -1
  36. data/lib/novelconverter.rb +8 -21
  37. data/lib/novelsetting.rb +79 -4
  38. data/lib/version.rb +1 -1
  39. data/lib/web/all.rb +12 -0
  40. data/lib/web/appserver.rb +612 -0
  41. data/lib/web/helper4web.rb +15 -0
  42. data/lib/web/progressbar4web.rb +32 -0
  43. data/lib/web/public/favicon.ico +0 -0
  44. data/lib/web/public/resources/bootbox.min.js +6 -0
  45. data/lib/web/public/resources/common.ui.js +143 -0
  46. data/lib/web/public/resources/dataTables.colVis.js +1113 -0
  47. data/lib/web/public/resources/help/rect_select.png +0 -0
  48. data/lib/web/public/resources/help/ssmain.png +0 -0
  49. data/lib/web/public/resources/help/tag.png +0 -0
  50. data/lib/web/public/resources/jquery.moveto.js +44 -0
  51. data/lib/web/public/resources/jquery.outerclick.js +60 -0
  52. data/lib/web/public/resources/jquery.slidenavbar.js +89 -0
  53. data/lib/web/public/resources/narou.library.js +815 -0
  54. data/lib/web/public/resources/narou.ui.js +993 -0
  55. data/lib/web/public/resources/perfect-scrollbar.min.css +5 -0
  56. data/lib/web/public/resources/perfect-scrollbar.min.js +4 -0
  57. data/lib/web/public/resources/shortcut.js +223 -0
  58. data/lib/web/public/resources/sort_asc.png +0 -0
  59. data/lib/web/public/resources/sort_desc.png +0 -0
  60. data/lib/web/public/resources/toggle-switch.css +322 -0
  61. data/lib/web/public/robots.txt +3 -0
  62. data/lib/web/public/test/jquery.outerclick.html +72 -0
  63. data/lib/web/pushserver.rb +110 -0
  64. data/lib/web/settingmessages.rb +14 -0
  65. data/lib/web/streaminginput.rb +103 -0
  66. data/lib/web/streaminglogger.rb +52 -0
  67. data/lib/web/views/about.haml +11 -0
  68. data/lib/web/views/help.haml +105 -0
  69. data/lib/web/views/index.haml +245 -0
  70. data/lib/web/views/js/widget.erb +74 -0
  71. data/lib/web/views/layout.haml +49 -0
  72. data/lib/web/views/novels/setting.haml +177 -0
  73. data/lib/web/views/settings.haml +115 -0
  74. data/lib/web/views/style.scss +737 -0
  75. data/lib/web/views/widget.haml +39 -0
  76. data/lib/web/web-socket-ruby/.gitignore +1 -0
  77. data/lib/web/web-socket-ruby/README.txt +75 -0
  78. data/lib/web/web-socket-ruby/lib/web_socket.rb +601 -0
  79. data/lib/web/web-socket-ruby/samples/chat_server.rb +58 -0
  80. data/lib/web/web-socket-ruby/samples/echo_server.rb +33 -0
  81. data/lib/web/web-socket-ruby/samples/stdio_client.rb +25 -0
  82. data/lib/web/worker.rb +87 -0
  83. data/narou.gemspec +36 -3
  84. data/narou.rb +8 -6
  85. data/preset/ncode.syosetu.com/n8725k/converter.rb +2 -1
  86. data/spec/data/convert_test/replace/correct_test_replace.txt +1 -1
  87. data/spec/data/convert_test/replace/test_replace.txt +1 -1
  88. data/spec/data/convert_test/ruby/correct_test_ruby.txt +18 -1
  89. data/spec/data/convert_test/ruby/test_ruby.txt +18 -0
  90. data/spec/downloader_spec.rb +37 -0
  91. data/spec/eventable_spec.rb +172 -0
  92. data/spec/exit_code_spec.rb +67 -0
  93. data/spec/helper_spec.rb +72 -0
  94. data/spec/input_spec.rb +76 -0
  95. data/spec/logger_spec.rb +53 -0
  96. data/spec/novelsetting_spec.rb +35 -0
  97. data/spec/worker_spec.rb +56 -0
  98. data/template/ibunko_novel.txt.erb +2 -2
  99. data/template/novel.txt.erb +2 -2
  100. metadata +213 -29
@@ -0,0 +1,44 @@
1
+ /* -*- coding: utf-8 -*-
2
+ *
3
+ * jquery.moveto.js
4
+ * Copyright 2014 whiteleaf. All rights reserved.
5
+ *
6
+ * Usage:
7
+ * $.moveTo({
8
+ * duration: 500
9
+ * });
10
+ *
11
+ * in your html:
12
+ * <a href="#section2" data-move-to="#section2">Jump</a>
13
+ * ==/snip/==
14
+ * <h2 id="section2">Section #2</h2>
15
+ */
16
+
17
+ (function($) {
18
+ "use strict";
19
+ // default options
20
+ var options = {
21
+ duration: "fast"
22
+ };
23
+ var touchable_device = "ontouchstart" in window;
24
+ var toggle_event_name = (touchable_device ? "touchstart" : "click");
25
+ var moveTo = {
26
+ initialize: function() {
27
+ var body_padding_top = parseInt($("body").css("padding-top"));
28
+ return $("[data-move-to]").on(toggle_event_name, function(e) {
29
+ e.preventDefault();
30
+ var target = $(this).data("moveTo");
31
+ var target_y = 0;
32
+ if (target !== "top") {
33
+ target_y = $(target).offset().top - body_padding_top;
34
+ }
35
+ $("html,body").animate({ scrollTop: target_y }, options.duration);
36
+ });
37
+ },
38
+ };
39
+ $.moveTo = function(opts) {
40
+ $.extend(options, opts);
41
+ return moveTo.initialize();
42
+ };
43
+ }(jQuery));
44
+
@@ -0,0 +1,60 @@
1
+ /* -*- coding: utf-8 -*-
2
+ *
3
+ * jquery.outerclick.js
4
+ * Copyright 2014 whiteleaf. All rights reserved.
5
+ *
6
+ * Usage:
7
+ * $("#menu").outerClick(function() {
8
+ * $(this).hide(); // do something
9
+ * });
10
+ * $("#menu").outerClickOne(function() {
11
+ * // once process
12
+ * });
13
+ * either
14
+ * $("#menu").outerClick(function(){}, "one");
15
+ */
16
+
17
+ (function($) {
18
+ "use strict";
19
+ var outerClick = function(obj, callback, bind_func) {
20
+ var self = this;
21
+ $(document)[bind_func]("click", function(e) {
22
+ var pos = self.getEventPosition(e);
23
+ var element = self.elementFromPoint(pos);
24
+ if (obj !== element) {
25
+ if (!self.searchTargetUpstream(element, obj)) {
26
+ callback.apply(obj);
27
+ }
28
+ }
29
+ });
30
+ };
31
+ $.extend(outerClick.prototype, {
32
+ elementFromPoint : function(pos) {
33
+ var doc = $(document);
34
+ var element = document.elementFromPoint(pos.x - doc.scrollLeft(),
35
+ pos.y - doc.scrollTop());
36
+ return element;
37
+ },
38
+ getEventPosition : function(event) {
39
+ return { x: event.pageX, y: event.pageY };
40
+ },
41
+ searchTargetUpstream : function(element, target) {
42
+ var parent = $(element).parent();
43
+ if (parent.length === 0) return null;
44
+ if (parent[0] === target) return parent;
45
+ return this.searchTargetUpstream(parent, target);
46
+ },
47
+ });
48
+ $.fn.outerClick = function(callback, bind_func) {
49
+ if (typeof bind_func === "undefined") bind_func = "on";
50
+ return this.each(function() {
51
+ new outerClick(this, callback, bind_func);
52
+ });
53
+ };
54
+ $.fn.outerClickOne = function(callback) {
55
+ return this.each(function() {
56
+ new outerClick(this, callback, "one");
57
+ });
58
+ }
59
+ }(jQuery));
60
+
@@ -0,0 +1,89 @@
1
+ /* -*- coding: utf-8 -*-
2
+ *
3
+ * jquery.slideNavbar.js
4
+ * Copyright 2014 whiteleaf. All rights reserved.
5
+ *
6
+ * Usage:
7
+ * var slideNavbar = $(".navbar-collapse").slideNavbar();
8
+ * slideNavbar.slide(); // slide nav manually
9
+ * slideNavbar.slide(true); // no animation
10
+ *
11
+ * in your css:
12
+ * @media (max-width:767px) {
13
+ * .navbar-collapse {
14
+ * position: absolute;
15
+ * right: -270px;
16
+ * width: 270px;
17
+ * }
18
+ * }
19
+ *
20
+ * Require:
21
+ * Bootstrap v3.2.0 or higher
22
+ */
23
+
24
+ (function($) {
25
+ "use strict";
26
+ // default options
27
+ var options = {
28
+ hiding_class: "slide-hiding-collapse",
29
+ width: 270,
30
+ duration: 250
31
+ };
32
+ var touchable_device = "ontouchstart" in window;
33
+ var toggle_event_name = (touchable_device ? "touchstart" : "click");
34
+ var slideNavbar = {
35
+ initialize: function(i, obj) {
36
+ var collapse = $(obj);
37
+ var self = this;
38
+ this.hiding_class = options.hiding_class;
39
+ this.collapse = collapse;
40
+ collapse.css({
41
+ "max-height": $(window).height() * 0.9,
42
+ });
43
+ $(".navbar-toggle").off("click");
44
+ $(".navbar-toggle").on(toggle_event_name, function(e) {
45
+ e.preventDefault();
46
+ e.stopPropagation();
47
+ self.slide();
48
+ });
49
+ collapse.addClass(self.hiding_class);
50
+ collapse.addClass("in"); // enable visible collpase nav
51
+ },
52
+ slide: function(force_close) {
53
+ var duration_org = options.duration;
54
+ if (this.collapse.hasClass(this.hiding_class) && !force_close) {
55
+ this.slideOpen();
56
+ this.collapse.removeClass(this.hiding_class);
57
+ }
58
+ else {
59
+ this.slideClose();
60
+ this.collapse.addClass(this.hiding_class);
61
+ }
62
+ },
63
+ slideOpen: function() {
64
+ this.collapse.stop().animate({right: "0"}, options.duration);
65
+ },
66
+ slideClose: function() {
67
+ this.collapse.stop().animate({
68
+ right: "-" + options.width + "px"
69
+ }, options.duration);
70
+ },
71
+ };
72
+ var Api = function() {};
73
+ Api.prototype.slide = function(force_close) {
74
+ if ($(".navbar-toggle").is(":visible")) {
75
+ slideNavbar.slide(force_close);
76
+ }
77
+ };
78
+ Api.prototype.close = function() {
79
+ slideNavbar.slide(true);
80
+ };
81
+ $.fn.slideNavbar = function(opts) {
82
+ $.extend(options, opts);
83
+ this.each(function(i, obj) {
84
+ slideNavbar.initialize(i, obj);
85
+ });
86
+ return new Api();
87
+ };
88
+ }(jQuery));
89
+
@@ -0,0 +1,815 @@
1
+ /* -*- coding: utf-8 -*-
2
+ *
3
+ * Copyright 2013 whiteleaf. All rights reserved.
4
+ */
5
+
6
+ var Narou = (function() {
7
+ "use strict";
8
+
9
+ var Narou = {};
10
+ var storage_cache = null;
11
+ var storage = null;
12
+
13
+ /*************************************************************************
14
+ * ローカルストレージ
15
+ *************************************************************************/
16
+ var Storage = Narou.Storage = function() {
17
+ this.initialize();
18
+ };
19
+
20
+ $.extend(Storage.prototype, {
21
+ storage_name: "Narou.rb_WEB_UI_saved",
22
+ initialize: function() {
23
+ this.objects = storage_cache ? storage_cache : this.load();
24
+ },
25
+
26
+ load: function() {
27
+ var objects = localStorage.getItem(this.storage_name);
28
+ return objects ? JSON.parse(objects) : {};
29
+ },
30
+
31
+ save: function() {
32
+ localStorage.setItem(this.storage_name, JSON.stringify(this.objects));
33
+ },
34
+
35
+ get: function(key) {
36
+ return this.objects[key];
37
+ },
38
+
39
+ set: function(key, value) {
40
+ this.objects[key] = value;
41
+ return this;
42
+ },
43
+ });
44
+
45
+ storage = new Storage();
46
+
47
+ /*************************************************************************
48
+ * ユーティリティ
49
+ *************************************************************************/
50
+ $.extend(Narou, {
51
+ registerCloseHandler: function(callback) {
52
+ // Chrome, IEですぐにclickイベントをバインドすると、メニュー表示時の
53
+ // クリックに反応してしまう(表示上のズレによって、クリック時のマウス
54
+ // 座標上に対象オブジェクトが存在しないため)ので、イベント作成をほんの
55
+ // 少し遅らせる
56
+ setTimeout(function() {
57
+ // 関係ないところをクリックした時に閉じる
58
+ $(document).one("click", callback);
59
+ }, 100);
60
+ },
61
+
62
+ popupMenu: function(menu_id, pos, close_menu_handler) {
63
+ var $menu = $(menu_id);
64
+ var left = $(window).width() < pos.x - $(document).scrollLeft() + $menu.outerWidth() ?
65
+ pos.x - $menu.outerWidth() : pos.x;
66
+ var top = $(window).height() < pos.y - $(document).scrollTop() + $menu.outerHeight() ?
67
+ pos.y - $menu.outerHeight() : pos.y;
68
+ $menu.show().offset({
69
+ left: left, top: top
70
+ });
71
+ Narou.registerCloseHandler(close_menu_handler);
72
+ },
73
+
74
+ // http://qiita.com/osakanafish/items/c64fe8a34e7221e811d0
75
+ formatDate: function(date, format) {
76
+ if (!format) format = 'YYYY-MM-DD hh:mm:ss.SSS';
77
+ format = format.replace(/YYYY/g, date.getFullYear());
78
+ format = format.replace(/MM/g, ('0' + (date.getMonth() + 1)).slice(-2));
79
+ format = format.replace(/DD/g, ('0' + date.getDate()).slice(-2));
80
+ format = format.replace(/hh/g, ('0' + date.getHours()).slice(-2));
81
+ format = format.replace(/mm/g, ('0' + date.getMinutes()).slice(-2));
82
+ format = format.replace(/ss/g, ('0' + date.getSeconds()).slice(-2));
83
+ if (format.match(/S/g)) {
84
+ var milliSeconds = ('00' + date.getMilliseconds()).slice(-3);
85
+ var length = format.match(/S/g).length;
86
+ for (var i = 0; i < length; i++) format = format.replace(/S/, milliSeconds.substring(i, i + 1));
87
+ }
88
+ return format;
89
+ },
90
+ });
91
+
92
+ /*************************************************************************
93
+ * Push 通知管理
94
+ *************************************************************************/
95
+ var Notification = Narou.Notification = function() {
96
+ this.initialize();
97
+ };
98
+
99
+ Notification.instance = function() {
100
+ if (!this.__instance) {
101
+ this.__instance = new this;
102
+ }
103
+ return this.__instance;
104
+ };
105
+
106
+ $.extend(Notification.prototype, {
107
+ RETRY_LIMIT: 5,
108
+ RETRY_WAIT: 2000,
109
+
110
+ initialize: function() {
111
+ this.events = {};
112
+ this.retry_count = this.RETRY_LIMIT;
113
+ this.connect();
114
+ },
115
+
116
+ connect: function() {
117
+ if (this.connection) return;
118
+ var self = this;
119
+ this.connected = false;
120
+ var connection = window.c = this.connection = new WebSocket(this.create_ws_uri());
121
+
122
+ connection.onopen = function() {
123
+ self.connected = true;
124
+ self.trigger("console.clear");
125
+ self.retry_count = self.RETRY_LIMIT; // 接続出来たらリトライカウント回復
126
+ };
127
+
128
+ connection.onclose = function() {
129
+ self.connection = null;
130
+ // PCのスリープ等でコネクションが切れた場合に再接続する
131
+ if (self.retry_count-- > 0) {
132
+ setTimeout(function() {
133
+ self.connected = false;
134
+ self.connect();
135
+ }, self.RETRY_WAIT);
136
+ }
137
+ };
138
+
139
+ connection.onmessage = function(e) {
140
+ if (e && e.data) {
141
+ self.onmessage(JSON.parse(e.data));
142
+ }
143
+ };
144
+ },
145
+
146
+ create_ws_uri: function() {
147
+ var host = location.hostname,
148
+ port = location.port;
149
+ return "ws://" + host + ":" + (parseInt(port) + 1) + "/";
150
+ },
151
+
152
+ onmessage: function(data) {
153
+ var self = this;
154
+ $.each(data, function(event, value) {
155
+ self.trigger(event, value);
156
+ });
157
+ },
158
+
159
+ on: function(event, block) {
160
+ if (typeof block !== "function") {
161
+ $.error("need a function");
162
+ }
163
+ var stack = this.events[event] || [];
164
+ stack.push(block);
165
+ this.events[event] = stack;
166
+ },
167
+
168
+ trigger: function(event, data) {
169
+ var self = this;
170
+ var stack = this.events[event] || [];
171
+ $.each(stack, function() {
172
+ this.call(self, data);
173
+ });
174
+ },
175
+
176
+ send: function(json) {
177
+ this.connection.send(JSON.stringify(json));
178
+ },
179
+ });
180
+
181
+ /*************************************************************************
182
+ * コンテキストメニュー
183
+ *************************************************************************/
184
+ var ContextMenu = Narou.ContextMenu = function(action, notification) {
185
+ this.action = action;
186
+ this.notification = notification;
187
+ this.closed = true;
188
+ this.initializeConsoleDialog();
189
+ this.initializeMenuEvent();
190
+ };
191
+
192
+ $.extend(ContextMenu.prototype, {
193
+ open: function(target_id, pos, callback) {
194
+ this.target_id = target_id;
195
+ if (!this.closed) {
196
+ // メニューを開いた状態で直接ボタンを押した場合に一旦閉じるイベントを起こさせる
197
+ $(document).trigger("click");
198
+ }
199
+ this.closed = false;
200
+ Narou.popupMenu("#context-menu", pos, function() {
201
+ $("#context-menu").hide();
202
+ if (typeof callback === "function") {
203
+ callback();
204
+ }
205
+ });
206
+ },
207
+
208
+ openConsoleDialog: function(callback) {
209
+ if (typeof callback !== "function") return;
210
+ var $console_dialog = $("#console-dialog");
211
+ $console_dialog.one("show.bs.modal", callback);
212
+ $(document).one("cancel.narou.remove", function() {
213
+ $console_dialog.modal("hide");
214
+ });
215
+ this.console.clear();
216
+ $console_dialog.modal();
217
+ },
218
+
219
+ initializeConsoleDialog: function() {
220
+ this.console = new Narou.Console(this.notification, {
221
+ restore: false, buttons: false,
222
+ id: "#each-console"
223
+ });
224
+ },
225
+
226
+ initializeMenuEvent: function() {
227
+ var $context_menu = $("#context-menu");
228
+ var self = this;
229
+ $("#context-menu-setting").on("click", function(e) {
230
+ e.preventDefault();
231
+ location.href = "/novels/" + self.target_id + "/setting";
232
+ });
233
+ $("#context-menu-update").on("click", function(e) {
234
+ e.preventDefault();
235
+ self.openConsoleDialog(function() {
236
+ self.action.update(self.target_id);
237
+ });
238
+ });
239
+ $("#context-menu-send").on("click", function(e) {
240
+ e.preventDefault();
241
+ self.openConsoleDialog(function() {
242
+ self.action.send(self.target_id);
243
+ });
244
+ });
245
+ $("#context-menu-freeze-toggle").on("click", function(e) {
246
+ e.preventDefault();
247
+ self.action.freeze(self.target_id);
248
+ });
249
+ $("#context-menu-remove").on("click", function(e) {
250
+ e.preventDefault();
251
+ self.openConsoleDialog(function() {
252
+ self.action.remove(self.target_id);
253
+ });
254
+ });
255
+ $("#context-menu-remove-with-file").on("click", function(e) {
256
+ e.preventDefault();
257
+ self.openConsoleDialog(function() {
258
+ self.action.removeWithFile(self.target_id);
259
+ });
260
+ });
261
+ $("#context-menu-convert").on("click", function(e) {
262
+ e.preventDefault();
263
+ self.openConsoleDialog(function() {
264
+ self.action.convert(self.target_id);
265
+ });
266
+ });
267
+ $("#context-menu-diff").on("click", function(e) {
268
+ e.preventDefault();
269
+ self.action.diff(self.target_id);
270
+ });
271
+ $("#context-menu-inspect").on("click", function(e) {
272
+ e.preventDefault();
273
+ self.openConsoleDialog(function() {
274
+ self.action.inspect(self.target_id);
275
+ });
276
+ });
277
+ $("#context-menu-folder").on("click", function(e) {
278
+ e.preventDefault();
279
+ self.action.folder(self.target_id);
280
+ });
281
+ $("#context-menu-backup").on("click", function(e) {
282
+ e.preventDefault();
283
+ self.openConsoleDialog(function() {
284
+ self.action.backup(self.target_id);
285
+ });
286
+ });
287
+ }
288
+ });
289
+
290
+ /*************************************************************************
291
+ * アクション
292
+ *************************************************************************/
293
+ var Action = Narou.Action = function(table) {
294
+ this.table = table;
295
+ };
296
+
297
+ $.extend(Action.prototype, {
298
+ _getSelectedIds: function(args) {
299
+ if (typeof args !== "undefined" && args.length > 0) {
300
+ return Array.prototype.slice.call(args);
301
+ }
302
+ var ids = [];
303
+ $.each(this.table.rows(".selected").data(), function(i, val) {
304
+ ids.push(val.id);
305
+ });
306
+ return ids;
307
+ },
308
+
309
+ selectAll: function() {
310
+ this.table.$("tr").addClass("selected");
311
+ this.table.fireChangeSelect();
312
+ },
313
+
314
+ selectView: function() {
315
+ $("#novel-list tbody tr").addClass("selected");
316
+ this.table.fireChangeSelect();
317
+ },
318
+
319
+ selectClear: function() {
320
+ this.table.$("tr.selected").removeClass("selected");
321
+ this.table.fireChangeSelect();
322
+ },
323
+
324
+ download: function() {
325
+ bootbox.prompt("ダウンロードする小説のURL、もしくはNコードを入力", function(target) {
326
+ if (!target) return;
327
+ $.post("/api/download", { "target": target });
328
+ console.log("new downloading %o", target);
329
+ });
330
+ },
331
+
332
+ downloadForce: function() {
333
+ var ids = this._getSelectedIds(arguments);
334
+ if (ids.length === 0) return;
335
+ $.post("/api/download_force", { "ids": ids });
336
+ console.log("force downloading " + ids.join(", "));
337
+ },
338
+
339
+ update: function() {
340
+ var ids = this._getSelectedIds(arguments);
341
+ if (ids.length === 0) {
342
+ $.post("/api/update");
343
+ console.log("updating all");
344
+ }
345
+ else {
346
+ $.post("/api/update_select", { "ids": ids });
347
+ console.log("updating " + ids.join(", "));
348
+ }
349
+ },
350
+
351
+ send: function() {
352
+ var ids = this._getSelectedIds(arguments);
353
+ if (ids.length === 0) {
354
+ $.post("/api/send");
355
+ console.log("sending all");
356
+ }
357
+ else {
358
+ $.post("/api/send_select", { "ids": ids });
359
+ console.log("sending " + ids.join(", "));
360
+ }
361
+ },
362
+
363
+ freeze: function() {
364
+ var ids = this._getSelectedIds(arguments);
365
+ if (ids.length === 0) return;
366
+ $.post("/api/freeze", { "ids": ids });
367
+ console.log("freezing(toggle) " + ids.join(", "));
368
+ },
369
+
370
+ freezeOn: function() {
371
+ var ids = this._getSelectedIds(arguments);
372
+ if (ids.length === 0) return;
373
+ $.post("/api/freeze_on", { "ids": ids });
374
+ console.log("freezing " + ids.join(", "));
375
+ },
376
+
377
+ freezeOff: function() {
378
+ var ids = this._getSelectedIds(arguments);
379
+ if (ids.length === 0) return;
380
+ $.post("/api/freeze_off", { "ids": ids });
381
+ console.log("thawing " + ids.join(", "));
382
+ },
383
+
384
+ _removeConfirmDialog: function(title, ids, callback) {
385
+ var message = "";
386
+ this.table.rows().data().each(function(data, idx) {
387
+ if (ids.indexOf(data.id + "") !== -1) {
388
+ message += "<li>" + data.title + "</li>";
389
+ }
390
+ });
391
+ message = '<div style="max-height:300px;overflow:auto"><ul>' + message + '</ul></div>';
392
+ bootbox.dialog({
393
+ title: title,
394
+ message: message,
395
+ buttons: {
396
+ danger: {
397
+ label: "削除する",
398
+ className: "btn-danger",
399
+ callback: function() {
400
+ callback(true);
401
+ $(document).trigger("ok.narou.remove");
402
+ }
403
+ },
404
+ main: {
405
+ label: "キャンセル",
406
+ className: "btn-default",
407
+ callback: function() {
408
+ callback(false);
409
+ $(document).trigger("cancel.narou.remove");
410
+ }
411
+ }
412
+ }
413
+ });
414
+ },
415
+
416
+ remove: function() {
417
+ var ids = this._getSelectedIds(arguments);
418
+ if (ids.length === 0) return;
419
+ this._removeConfirmDialog("選択した小説を削除しますか?", ids, function(result) {
420
+ if (!result) return;
421
+ $.post("/api/remove", { "ids": ids });
422
+ console.log("removing " + ids.join(", "));
423
+ });
424
+ },
425
+
426
+ removeWithFile: function() {
427
+ var ids = this._getSelectedIds(arguments);
428
+ if (ids.length === 0) return;
429
+ this._removeConfirmDialog("選択した小説を“完全に”削除しますか?", ids, function(result) {
430
+ if (!result) return;
431
+ $.post("/api/remove_with_file", { "ids": ids });
432
+ console.log("removing with file " + ids.join(", "));
433
+ });
434
+ },
435
+
436
+ convert: function() {
437
+ var ids = this._getSelectedIds(arguments);
438
+ if (ids.length === 0) return;
439
+ $.post("/api/convert", { "ids": ids });
440
+ console.log("converting " + ids.join(", "));
441
+ },
442
+
443
+ diff: function() {
444
+ var ids = this._getSelectedIds(arguments);
445
+ if (ids.length === 0) return;
446
+ $.post("/api/diff", { "ids": ids });
447
+ console.log("diffing " + ids.join(", "));
448
+ },
449
+
450
+ inspect: function() {
451
+ var ids = this._getSelectedIds(arguments);
452
+ if (ids.length === 0) return;
453
+ $.post("/api/inspect", { "ids": ids });
454
+ console.log("inspecting " + ids.join(", "));
455
+ },
456
+
457
+ folder: function() {
458
+ var ids = this._getSelectedIds(arguments);
459
+ if (ids.length === 0) return;
460
+ $.post("/api/folder", { "ids": ids });
461
+ console.log("opening folder " + ids.join(", "));
462
+ },
463
+
464
+ backup: function() {
465
+ var ids = this._getSelectedIds(arguments);
466
+ if (ids.length === 0) return;
467
+ $.post("/api/backup", { "ids": ids });
468
+ console.log("backup " + ids.join(", "));
469
+ },
470
+ });
471
+
472
+ /*************************************************************************
473
+ * コンソール
474
+ *************************************************************************/
475
+ var Console = Narou.Console = function(notification, options) {
476
+ this.options = $.extend({
477
+ restore: true, // コンソールの大きさを復元・保存するか
478
+ buttons: true, // 拡大縮小等のコントロールボタンを使用するか
479
+ id: "#console", // コンソールのID名
480
+ buttons_id: "#console-buttons" // コントロールボタンを格納している要素のID名
481
+ }, options);
482
+ this.initialize(notification);
483
+ };
484
+
485
+ $.extend(Console.prototype, {
486
+ animate_duration: 200,
487
+
488
+ initialize: function(notification) {
489
+ this.notification = notification;
490
+ this.last_char_was_return = true;
491
+ this.console = $(this.options.id);
492
+ this.init_scrollbar();
493
+ if (this.options.buttons) this.init_buttons();
494
+ this.init_events();
495
+ this.init_notification();
496
+ if (this.options.restore) this.restore_console_shape();
497
+ },
498
+
499
+ init_scrollbar: function() {
500
+ this.console.perfectScrollbar({
501
+ wheelspeed: 80,
502
+ suppressScrollX: true,
503
+ minScrollbarLength: 20,
504
+ });
505
+ this.original_height = this.console.height();
506
+ },
507
+
508
+ init_buttons: function() {
509
+ var self = this;
510
+ this.console.css("min-height", this.original_height);
511
+ $(this.options.buttons_id + " .console-expand").on("click", function(e) {
512
+ self.expand_console();
513
+ });
514
+ $(this.options.buttons_id + " .console-trash").on("click", function(e) {
515
+ self.trash_console();
516
+ });
517
+ },
518
+
519
+ init_events: function() {
520
+ this.manage_resize_event();
521
+ var self = this;
522
+ this.console.on("resize", function() {
523
+ if (!self.options.restore) return;
524
+ var data = {
525
+ height: self.console.height(),
526
+ expanded: self.console.hasClass("expanded")
527
+ };
528
+ storage.set("console", data);
529
+ storage.save();
530
+ });
531
+ this.init_events_progressbar();
532
+ this.notification.on("console.clear", function() {
533
+ self.clear();
534
+ });
535
+ },
536
+
537
+ init_events_progressbar: function() {
538
+ var self = this;
539
+ var create_progress_html = function() {
540
+ return '<div class="progress"><div class="progress-bar progress-bar-success progress-bar-striped active" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%"></div></div>';
541
+ };
542
+ var $progress = null;
543
+ var setProgressValue = function(step) {
544
+ $progress.attr("aria-valuenow", step)
545
+ .width(step + "%");
546
+ };
547
+ this.notification.on("progressbar.init", function() {
548
+ self.puts(create_progress_html());
549
+ $progress = self.console.find(".progress > div");
550
+ });
551
+ this.notification.on("progressbar.step", function(value) {
552
+ if (!$progress) return;
553
+ setProgressValue(value);
554
+ });
555
+ this.notification.on("progressbar.clear", function() {
556
+ if (!$progress) return;
557
+ // 表示のタイムラグで100%付近のが表示されないまま消えてしまうので、
558
+ // 演出的にプログレスバーの消去を遅らせて、100%付近まで表示する
559
+ setProgressValue(100);
560
+ $progress = null;
561
+ setTimeout(function() {
562
+ $(".progress").parent("div").remove();
563
+ }, 500);
564
+ });
565
+ },
566
+
567
+ init_notification: function() {
568
+ var self = this;
569
+ this.notification.on("echo", function(message) {
570
+ self.print(message);
571
+ });
572
+ },
573
+
574
+ manage_resize_event: function() {
575
+ /*
576
+ var con = this.console;
577
+ con.data('x', con.outerWidth());
578
+ con.data('y', con.outerHeight());
579
+ $(document).on("mouseup", function () {
580
+ if (con.outerWidth() != con.data('x') || con.outerHeight() != con.data('y')) {
581
+ con.trigger("resize");
582
+ }
583
+ con.data('x', con.outerWidth());
584
+ con.data('y', con.outerHeight());
585
+ });
586
+ */
587
+ },
588
+
589
+ restore_console_shape: function() {
590
+ var data = storage.get("console");
591
+ if (data) {
592
+ this.console.height(data.height);
593
+ if (data.expanded) this.toggle_expanded();
594
+ }
595
+ },
596
+
597
+ scroll_to_bottom: function() {
598
+ var con = this.console;
599
+ con.scrollTop(con.prop("scrollHeight"));
600
+ },
601
+
602
+ position_is_bottom: function() {
603
+ var con = this.console;
604
+ return (con.scrollTop() === con.prop("scrollHeight") - con.outerHeight());
605
+ },
606
+
607
+ print: function(str) {
608
+ var self = this;
609
+ var con = this.console;
610
+ var last_char = str[str.length - 1];
611
+ var lines = str.split("\n");
612
+ var was_bottom = this.position_is_bottom();
613
+ var last_char_is_return = last_char === "\n";
614
+ if (last_char_is_return) {
615
+ lines = lines.slice(0, -1);
616
+ }
617
+ if (!this.last_char_was_return) {
618
+ var last = con.find("div.console-line:last-child");
619
+ var char = lines.pop();
620
+ if (char) last.append(char);
621
+ }
622
+ $.each(lines, function() {
623
+ con.append("<div class=console-line>" + this);
624
+ });
625
+ con.perfectScrollbar("update");
626
+ // 表示する段階で最下部までスクロールしてあった場合はスクロールする
627
+ if (was_bottom) {
628
+ this.scroll_to_bottom();
629
+ }
630
+ this.last_char_was_return = last_char_is_return;
631
+ },
632
+
633
+ puts: function(str) {
634
+ if (str[str.length - 1] !== "\n") {
635
+ this.print(str.concat("\n"));
636
+ }
637
+ else {
638
+ this.print(str);
639
+ }
640
+ },
641
+
642
+ expand_console: function() {
643
+ var self = this;
644
+ var calced_height;
645
+ if (this.console.hasClass("expanded")) {
646
+ calced_height = this.original_height;
647
+ }
648
+ else {
649
+ var top = this.console.offset().top;
650
+ calced_height = ($(window).height() - top) * 0.8;
651
+ }
652
+ var was_bottom = this.position_is_bottom();
653
+ this.console.stop().animate({ height: calced_height }, this.animate_duration,
654
+ // after do
655
+ function() {
656
+ if (was_bottom) self.scroll_to_bottom();
657
+ self.console.trigger("resize");
658
+ self.console.perfectScrollbar("update");
659
+ });
660
+ this.toggle_expanded();
661
+ },
662
+
663
+ toggle_expanded: function() {
664
+ this.console.toggleClass("expanded");
665
+ $(".console-expand > span").toggleClass("hide");
666
+ },
667
+
668
+ // コンソールのログを削除すると同時に、サーバの履歴も削除する
669
+ trash_console: function() {
670
+ this.clear();
671
+ $.post("/api/clear_history");
672
+ },
673
+
674
+ clear: function() {
675
+ this.console.find("div.console-line").remove();
676
+ }
677
+ });
678
+
679
+ /*************************************************************************
680
+ * タグ機能
681
+ *************************************************************************/
682
+ var Tag = Narou.Tag = (function(table) {
683
+ this.table = table;
684
+ this.updateCanvas();
685
+ });
686
+
687
+ $.extend(Tag.prototype, {
688
+ updateCanvas: function() {
689
+ var $canvas = $("#tag-list-canvas");
690
+ this.registerEvents($canvas);
691
+ $.get("/api/tag_list", function(source) {
692
+ $canvas.html(source);
693
+ });
694
+ },
695
+
696
+ registerEvents: function($target, stop_bubbling) {
697
+ var self = this;
698
+ if (typeof stop_bubbling === "undefined") stop_bubbling = true;
699
+ var args = { stop_bubbling: stop_bubbling };
700
+ $target.on("click", ".tag", args, function(e) {
701
+ if (e.data.stop_bubbling) e.stopPropagation();
702
+ var tag_name = $(this).data("tag");
703
+ self.table.column("tags:name").search(tag_name).draw();
704
+ }).on("mousedown", ".tag", args, function(e) {
705
+ // 範囲選択モードでもクリック出来るように
706
+ if (e.data.stop_bubbling) e.stopPropagation();
707
+ });
708
+ },
709
+
710
+ openEditor: function() {
711
+ var ids = Action.prototype._getSelectedIds.call(this);
712
+ if (ids.length === 0) return;
713
+ this._createEditorField(ids, function(field) {
714
+ bootbox.dialog({
715
+ title: "タグの編集",
716
+ message: field,
717
+ buttons: {
718
+ cancel: {
719
+ label: "キャンセル",
720
+ className: "btn-default",
721
+ callback: function() {
722
+ }
723
+ },
724
+ main: {
725
+ label: "適用",
726
+ className: "btn-primary",
727
+ callback: function() {
728
+ var states = {};
729
+ var new_tag = $("#new-tag").val();
730
+ $("#tag-editor-field input[type=checkbox]").each(function(i, v) {
731
+ states[$(v).data("tagname")] = $(v).data("checkState");
732
+ });
733
+ if (new_tag) {
734
+ states[new_tag] = 2;
735
+ }
736
+ $.post("/api/edit_tag", {
737
+ ids: ids,
738
+ states: states
739
+ });
740
+ }
741
+ }
742
+ }
743
+ });
744
+ });
745
+ },
746
+
747
+ _createEditorField: function(ids, callback) {
748
+ var field = $("<div id=tag-editor-field class=form-group>");
749
+ var self = this;
750
+ var ids_count = ids.length;
751
+
752
+ function calcState(count) {
753
+ if (count === 0) {
754
+ return 0;
755
+ }
756
+ else if (count < ids_count) {
757
+ return 1;
758
+ }
759
+ else {
760
+ return 2;
761
+ }
762
+ }
763
+
764
+ $.post("/api/taginfo.json", { ids: ids }, function(taginfo) {
765
+ $.each(taginfo, function(tagname, info) {
766
+ var label = $('<label><input type="checkbox" data-tagname="' + tagname.replace(/"/g, "&quot;") +
767
+ '" data-default-checkstate=' + calcState(info.count) + '> ' + info.html + '&nbsp;&nbsp;</label>');
768
+ field.append(label);
769
+ });
770
+ var input = $('<div><input type="text" id="new-tag" placeholder="新規タグ" class="form-control"></div>' +
771
+ '<div><small>(複数追加する場合は半角スペースで区切る)</small></div>');
772
+ field.append(input);
773
+ self._registerEventsForEditorField(field);
774
+ callback(field);
775
+ });
776
+ },
777
+
778
+ _registerEventsForEditorField: function(field) {
779
+ function setCheckState(element, state) {
780
+ switch (state) {
781
+ case 0:
782
+ element.prop("indeterminate", false);
783
+ element.prop("checked", false);
784
+ break;
785
+ case 1:
786
+ if (element.data("defaultCheckstate") == 1) {
787
+ element.prop("indeterminate", true);
788
+ element.prop("checked", false);
789
+ break;
790
+ }
791
+ state++;
792
+ case 2:
793
+ element.prop("indeterminate", false);
794
+ element.prop("checked", true);
795
+ break;
796
+ }
797
+ element.data("checkState", state);
798
+ }
799
+
800
+ field.find("input[type=checkbox]")
801
+ .on("click", function(e) {
802
+ var elm = $(e.target);
803
+ var next_state = (elm.data("checkState") + 1) % 3;
804
+ setCheckState(elm, next_state);
805
+ })
806
+ .each(function(i, v) {
807
+ var elm = $(v);
808
+ setCheckState(elm, elm.data("defaultCheckstate"));
809
+ });
810
+ },
811
+ });
812
+
813
+ return Narou;
814
+ })();
815
+