narou 2.8.3.1 → 2.9.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/ChangeLog.md +77 -0
  3. data/Gemfile +2 -1
  4. data/README.md +69 -86
  5. data/lib/command/convert.rb +10 -11
  6. data/lib/command/init.rb +2 -2
  7. data/lib/command/list.rb +12 -3
  8. data/lib/command/setting.rb +20 -5
  9. data/lib/command/update.rb +0 -3
  10. data/lib/command/version.rb +2 -3
  11. data/lib/command/web.rb +58 -7
  12. data/lib/converterbase.rb +7 -24
  13. data/lib/database.rb +7 -6
  14. data/lib/device.rb +15 -1
  15. data/lib/device/ibunko.rb +1 -2
  16. data/lib/device/library/mac.rb +7 -0
  17. data/lib/downloader.rb +44 -21
  18. data/lib/extension.rb +25 -0
  19. data/lib/helper.rb +4 -3
  20. data/lib/html.rb +18 -2
  21. data/lib/narou.rb +20 -0
  22. data/lib/novelconverter.rb +11 -35
  23. data/lib/novelinfo.rb +19 -8
  24. data/lib/novelsetting.rb +0 -6
  25. data/lib/version.rb +1 -1
  26. data/lib/web/appserver.rb +134 -15
  27. data/lib/web/public/favicon.ico +0 -0
  28. data/lib/web/public/resources/common.ui.js +2 -2
  29. data/lib/web/public/resources/narou.library.js +657 -91
  30. data/lib/web/public/resources/narou.queue.js +1 -1
  31. data/lib/web/public/resources/narou.ui.js +253 -102
  32. data/lib/web/public/resources/sprintf.js +245 -0
  33. data/lib/web/pushserver.rb +14 -3
  34. data/lib/web/views/_about.haml +188 -0
  35. data/lib/web/views/{diff_list.haml → _diff_list.haml} +0 -0
  36. data/lib/web/views/{edit_replace_txt.haml → _edit_replace_txt.haml} +4 -4
  37. data/lib/web/views/_rebooting.haml +18 -0
  38. data/lib/web/views/bookmarklet/download.js.erb +2 -2
  39. data/lib/web/views/bookmarklet/insert_button.js.erb +1 -1
  40. data/lib/web/views/edit_menu.haml +223 -0
  41. data/lib/web/views/help.haml +29 -12
  42. data/lib/web/views/index.haml +99 -88
  43. data/lib/web/views/layout.haml +5 -3
  44. data/lib/web/views/notepad.haml +39 -0
  45. data/lib/web/views/novels/setting.haml +15 -5
  46. data/lib/web/views/settings.haml +2 -2
  47. data/lib/web/views/style.scss +72 -21
  48. data/lib/web/views/widget/download.haml +3 -2
  49. data/lib/web/views/widget/drag_and_drop.haml +3 -2
  50. data/lib/web/views/widget/notepad.haml +44 -0
  51. data/narou.gemspec +75 -6
  52. data/narou.rb +8 -14
  53. data/preset/ncode.syosetu.com/n5115cq/converter.rb +30 -0
  54. data/preset/ncode.syosetu.com/n7594ct/converter.rb +37 -0
  55. data/preset/ncode.syosetu.com/n8725k/converter.rb +1 -1
  56. data/preset/vertical_font.css +0 -10
  57. data/spec/generator/convert_spec_gen.rb +2 -0
  58. data/template/novel.txt.erb +3 -0
  59. data/webnovel/https.syosetu.org.yaml +1 -1
  60. data/webnovel/kakuyomu.jp.yaml +82 -0
  61. data/webnovel/ncode.syosetu.com.yaml +1 -0
  62. data/webnovel/syosetu.org.yaml +1 -1
  63. metadata +89 -12
  64. data/lib/web/views/about.haml +0 -85
  65. data/preset/DMincho.ttf +0 -0
Binary file
@@ -24,9 +24,9 @@ $(document).ready(function() {
24
24
  $.moveTo();
25
25
 
26
26
  /*
27
- * 自然に消える alert
27
+ * 自然に消えるフラッシュメッセージ
28
28
  */
29
- $("#fadeout-alert").delay(2000).animate({ opacity: "hide" }, 1500);
29
+ Narou.Flash.setEvents(".fadeout-alert");
30
30
 
31
31
  /*************************************************************************
32
32
  * Webサーバの方から入力を求められた時にモーダルを表示して返事を返す
@@ -184,21 +184,28 @@ var Narou = (function() {
184
184
  });
185
185
  },
186
186
 
187
- on: function(event, block) {
187
+ on: function(event, block, once) {
188
188
  if (typeof block !== "function") {
189
189
  $.error("need a function");
190
190
  }
191
191
  var stack = this.events[event] || [];
192
- stack.push(block);
192
+ stack.push([block, once]);
193
193
  this.events[event] = stack;
194
194
  },
195
195
 
196
+ one: function(event, block) {
197
+ this.on(event, block, true);
198
+ },
199
+
196
200
  trigger: function(event, data) {
197
201
  var self = this;
198
202
  var stack = this.events[event] || [];
199
- $.each(stack, function() {
200
- this.call(self, data);
201
- });
203
+ this.events[event] =
204
+ _.reject(stack, function(pair) {
205
+ var block = pair[0], once = pair[1];
206
+ block.call(self, data);
207
+ return once;
208
+ });
202
209
  },
203
210
 
204
211
  send: function(json) {
@@ -207,24 +214,25 @@ var Narou = (function() {
207
214
  });
208
215
 
209
216
  /*************************************************************************
210
- * コンテキストメニュー
217
+ * 個別メニュー
211
218
  *************************************************************************/
212
- var ContextMenu = Narou.ContextMenu = function(action, notification, tag) {
219
+ var ContextMenu = Narou.ContextMenu = function(action, tag) {
213
220
  this.action = action;
214
- this.notification = notification;
221
+ this.notification = Notification.instance();
215
222
  this.tag = tag;
216
223
  this.closed = true;
217
224
  this.initializeConsoleDialog();
218
- this.initializeMenuEvent();
219
225
  this.initializeDiffListEvent();
226
+ this.initializeMenu();
220
227
  };
221
228
 
222
229
  $.extend(ContextMenu.prototype, {
223
230
  open: function(target_id, pos, callback) {
231
+ var self = this;
224
232
  this.target_id = target_id;
225
233
  if (!this.closed) {
226
234
  // メニューを開いた状態で直接ボタンを押した場合に一旦閉じるイベントを起こさせる
227
- $(document).trigger("click");
235
+ this.close();
228
236
  }
229
237
  this.closed = false;
230
238
  var caller = function() {
@@ -232,15 +240,30 @@ var Narou = (function() {
232
240
  callback();
233
241
  };
234
242
  $(document).one("show.bs.dropdown", function() {
235
- $("#context-menu").hide();
243
+ self.object.hide();
244
+ self.closed = true;
236
245
  caller();
237
246
  });
238
- Narou.popupMenu("#context-menu", pos, function() {
239
- $("#context-menu").hide();
247
+ Narou.popupMenu(this.object, pos, function() {
248
+ self.object.hide();
249
+ self.closed = true;
240
250
  caller();
241
251
  });
242
252
  },
243
253
 
254
+ close: function() {
255
+ $(document).trigger("click");
256
+ this.closed = true;
257
+ },
258
+
259
+ save: function(text) {
260
+ this.text = text;
261
+ storage.set("context_menu_text", text);
262
+ storage.save();
263
+ this.object.remove();
264
+ this.initializeMenu();
265
+ },
266
+
244
267
  openConsoleDialog: function(callback) {
245
268
  if (typeof callback !== "function") return;
246
269
  var $console_dialog = $("#console-dialog");
@@ -253,7 +276,7 @@ var Narou = (function() {
253
276
  },
254
277
 
255
278
  initializeConsoleDialog: function() {
256
- this.console = new Narou.Console(this.notification, {
279
+ this.console = new Narou.Console({
257
280
  restore: false, buttons: false,
258
281
  id: "#each-console"
259
282
  });
@@ -298,66 +321,160 @@ var Narou = (function() {
298
321
  });
299
322
  },
300
323
 
301
- initializeMenuEvent: function() {
302
- var $context_menu = $("#context-menu");
303
- var self = this;
304
- $("#context-menu-setting").on("click", function(e) {
305
- e.preventDefault();
306
- location.href = "/novels/" + self.target_id + "/setting";
307
- });
308
- $("#context-menu-update").on("click", function(e) {
309
- e.preventDefault();
310
- self.openConsoleDialog(function() {
311
- self.action.update(self.target_id);
312
- });
313
- });
314
- $("#context-menu-send").on("click", function(e) {
315
- e.preventDefault();
316
- self.openConsoleDialog(function() {
317
- self.action.send(self.target_id);
318
- });
319
- });
320
- $("#context-menu-freeze-toggle").on("click", function(e) {
321
- e.preventDefault();
322
- self.action.freeze(self.target_id);
323
- });
324
- $("#context-menu-remove").on("click", function(e) {
325
- e.preventDefault();
326
- self.openConsoleDialog(function() {
327
- self.action.remove(self.target_id);
328
- });
329
- });
330
- $("#context-menu-edit-tag").on("click", function(e) {
331
- e.preventDefault();
332
- self.tag.openEditor(self.target_id);
333
- });
334
- $("#context-menu-convert").on("click", function(e) {
335
- e.preventDefault();
336
- self.openConsoleDialog(function() {
337
- self.action.convert(self.target_id);
338
- });
339
- });
340
- $("#context-menu-diff").on("click", function(e) {
341
- e.preventDefault();
342
- self.openSelectDiffListDialog(self.target_id);
324
+ initializeMenu: function() {
325
+ this.initializeMenuObject();
326
+ this.initializeMenuEvents();
327
+ $("body").append(this.object);
328
+ },
329
+
330
+ initializeMenuObject: function(text) {
331
+ this.text = text || storage.get("context_menu_text") || this.createDefaultMenuText();
332
+ this.object = this.createMenuObject(this.text);
333
+ },
334
+
335
+ createMenuObject: function(text) {
336
+ var object = $('<ul class="context-menu dropdown-menu" role="menu">');
337
+ _.each(text.split("\n"), function(line) {
338
+ var splited = line.split("<>");
339
+ var label = splited[0], command = splited[1];
340
+ var child;
341
+ if (!command) return;
342
+ if (command === "divider")
343
+ child = $('<li class="divider">');
344
+ else
345
+ child = $(sprintf('<li class="context-menu-%s"><a href="#">%s</a>', command, label));
346
+ object.append(child);
343
347
  });
344
- $("#context-menu-inspect").on("click", function(e) {
345
- e.preventDefault();
346
- self.openConsoleDialog(function() {
347
- self.action.inspect(self.target_id);
348
+ return object;
349
+ },
350
+
351
+ _default_commands: [
352
+ "setting", "diff", "edit_tag", "freeze_toggle", "update",
353
+ "send", "remove", "convert", "inspect", "folder", "backup"
354
+ ],
355
+
356
+ items: [
357
+ { label: "――――――――(区切り)", command: "divider" },
358
+ { label: "小説の変換設定", command: "setting" },
359
+ { label: "差分を表示", command: "diff" },
360
+ { label: "タグを編集", command: "edit_tag" },
361
+ { label: "凍結 or 解凍", command: "freeze_toggle" },
362
+ { label: "更新", command: "update" },
363
+ { label: "凍結済みでも更新", command: "update_force" },
364
+ { label: "送信", command: "send" },
365
+ { label: "削除", command: "remove" },
366
+ { label: "変換", command: "convert" },
367
+ { label: "調査状況ログを表示", command: "inspect" },
368
+ { label: "保存フォルダを開く", command: "folder" },
369
+ { label: "バックアップを作成", command: "backup" },
370
+ { label: "再ダウンロード", command: "download_force" },
371
+ ],
372
+
373
+ events: {
374
+ setting: function() {
375
+ var setting_page_path = "/novels/" + this.target_id + "/setting";
376
+ if (storage.get("open_new_tab_setting_pages")) {
377
+ window.open(setting_page_path);
378
+ }
379
+ else {
380
+ location.href = setting_page_path;
381
+ }
382
+ },
383
+
384
+ update: function() {
385
+ this.openConsoleDialog(function() {
386
+ this.action.update(this.target_id);
387
+ }.bind(this));
388
+ },
389
+
390
+ update_force: function() {
391
+ this.openConsoleDialog(function() {
392
+ this.action.updateForce(this.target_id);
393
+ }.bind(this));
394
+ },
395
+
396
+ send: function() {
397
+ this.openConsoleDialog(function() {
398
+ this.action.send(this.target_id);
399
+ }.bind(this));
400
+ },
401
+
402
+ freeze_toggle: function() {
403
+ this.action.freeze(this.target_id);
404
+ },
405
+
406
+ remove: function() {
407
+ this.openConsoleDialog(function() {
408
+ this.action.remove(this.target_id);
409
+ }.bind(this));
410
+ },
411
+
412
+ edit_tag: function() {
413
+ this.tag.openEditor(this.target_id);
414
+ },
415
+
416
+ convert: function() {
417
+ this.openConsoleDialog(function() {
418
+ this.action.convert(this.target_id);
419
+ }.bind(this));
420
+ },
421
+
422
+ diff: function() {
423
+ this.openSelectDiffListDialog(this.target_id);
424
+ },
425
+
426
+ inspect: function() {
427
+ this.openConsoleDialog(function() {
428
+ this.action.inspect(this.target_id);
429
+ }.bind(this));
430
+ },
431
+
432
+ folder: function() {
433
+ this.action.folder(this.target_id);
434
+ },
435
+
436
+ backup: function() {
437
+ this.openConsoleDialog(function() {
438
+ this.action.backup(this.target_id);
439
+ }.bind(this));
440
+ },
441
+
442
+ download_force: function() {
443
+ this.openConsoleDialog(function() {
444
+ this.action.downloadForce(this.target_id);
445
+ }.bind(this));
446
+ },
447
+ },
448
+
449
+ initializeMenuEvents: function() {
450
+ var object = this.object;
451
+ var self = this;
452
+ _.each(this.events, function(fn, command) {
453
+ object.find(".context-menu-" + command).on("click", function(e) {
454
+ e.preventDefault();
455
+ if (self.action)
456
+ fn.call(self);
348
457
  });
349
458
  });
350
- $("#context-menu-folder").on("click", function(e) {
351
- e.preventDefault();
352
- self.action.folder(self.target_id);
459
+ },
460
+
461
+ findItem: function(command) {
462
+ return this.items.find(function(item) {
463
+ return item.command === command;
353
464
  });
354
- $("#context-menu-backup").on("click", function(e) {
355
- e.preventDefault();
356
- self.openConsoleDialog(function() {
357
- self.action.backup(self.target_id);
358
- });
465
+ },
466
+
467
+ createDefaultMenuText: function() {
468
+ var self = this;
469
+ var menu_text_lines = [];
470
+ _.each(this._default_commands, function(command) {
471
+ var item = self.findItem(command);
472
+ if (!item)
473
+ $.error("invalid command(" + command + ")");
474
+ menu_text_lines.push(item.label + "<>" + command);
359
475
  });
360
- }
476
+ return menu_text_lines.join("\n");
477
+ },
361
478
  });
362
479
 
363
480
  /*************************************************************************
@@ -457,10 +574,10 @@ var Narou = (function() {
457
574
 
458
575
  updateGeneralLastup: function() {
459
576
  bootbox.dialog({
460
- title: '最新話掲載日の更新',
461
- message: "凍結済みを除く各小説の最新話掲載日を更新します。<br>" +
462
- "最新話掲載日は通常時のUPDATEでも更新されるので、手動で更新する必要は基本的にはありません。<br>" +
463
- "掲載日だけを調べて、選択的にUPDATEをかけるなど、用途を理解した上で小説サイトのサーバーに負荷をかけない範囲でご利用下さい。",
577
+ title: "最新話掲載日の更新",
578
+ message: "凍結済みを除く各小説の最新話掲載日のみを更新して反映させます。<br>" +
579
+ "最新話掲載日は通常時のUpdateでも更新されるので、手動で更新する必要は基本的にはありません。<br>" +
580
+ "掲載日だけを調べて、選択的にUpdateをかけるなど、用途を理解した上で小説サイトのサーバーに負荷をかけない範囲でご利用下さい。",
464
581
  backdrop: true,
465
582
  buttons: {
466
583
  cancel: {
@@ -606,26 +723,94 @@ var Narou = (function() {
606
723
  });
607
724
  });
608
725
  },
726
+
727
+ rebootDialog: function() {
728
+ var self = this;
729
+ bootbox.dialog({
730
+ title: '<span class="glyphicon glyphicon-refresh"></span> 再起動',
731
+ message: "<p>Narou.rb WEB UI サーバを再起動します。<br>" +
732
+ "バージョンを更新してある場合は最新バージョンで立ち上がります。</p>" +
733
+ "<p class=text-danger>アップデート中や変換中の小説がある場合は中断されます。<br>" +
734
+ "終わったかどうか確認しましょう。</p>",
735
+ backdrop: true,
736
+ buttons: {
737
+ danger: {
738
+ label: "再起動",
739
+ className: "btn-danger",
740
+ callback: function() {
741
+ self.reboot();
742
+ }
743
+ },
744
+ main: {
745
+ label: "キャンセル",
746
+ className: "btn-default",
747
+ }
748
+ }
749
+ });
750
+ },
751
+
752
+ reboot: function() {
753
+ $.post("/reboot", function(data) {
754
+ bootbox.hideAll();
755
+ bootbox.dialog({
756
+ title: "",
757
+ closeButton: false,
758
+ message: data
759
+ });
760
+ });
761
+ },
762
+
763
+ checkUpdatedSystem: function(funcs) {
764
+ $.post("/check_already_update_system", function(data) {
765
+ if (data.result) {
766
+ $.post("/gem_update_last_log", function(log) {
767
+ funcs.already_updated(log);
768
+ });
769
+ }
770
+ else {
771
+ funcs.not_updated();
772
+ }
773
+ });
774
+ },
775
+
776
+ updateSystem: function(callback) {
777
+ var notification = Notification.instance();
778
+ notification.one("server.update.success", function(log) {
779
+ callback("success", log);
780
+ });
781
+ notification.one("server.update.nothing", function(log) {
782
+ callback("nothing", log);
783
+ });
784
+ notification.one("server.update.failure", function(log) {
785
+ callback("failure", log);
786
+ });
787
+ $.post("/update_system");
788
+ },
789
+
790
+ eject: function() {
791
+ $.post("/api/eject");
792
+ },
793
+
609
794
  });
610
795
 
611
796
  /*************************************************************************
612
797
  * コンソール
613
798
  *************************************************************************/
614
- var Console = Narou.Console = function(notification, options) {
799
+ var Console = Narou.Console = function(options) {
615
800
  this.options = $.extend({
616
801
  restore: true, // コンソールの大きさを復元・保存するか
617
802
  buttons: true, // 拡大縮小等のコントロールボタンを使用するか
618
803
  id: "#console", // コンソールのID名
619
804
  buttons_id: "#console-buttons" // コントロールボタンを格納している要素のID名
620
805
  }, options);
621
- this.initialize(notification);
806
+ this.initialize();
622
807
  };
623
808
 
624
809
  $.extend(Console.prototype, {
625
810
  animate_duration: 200,
626
811
 
627
- initialize: function(notification) {
628
- this.notification = notification;
812
+ initialize: function() {
813
+ this.notification = Notification.instance();
629
814
  this.last_char_was_return = true;
630
815
  this.console = $(this.options.id);
631
816
  this.init_scrollbar();
@@ -820,14 +1005,197 @@ var Narou = (function() {
820
1005
  }
821
1006
  });
822
1007
 
1008
+ /*************************************************************************
1009
+ * 検索機能
1010
+ * dataTables の検索に、タグ検索機能を追加する
1011
+ *************************************************************************/
1012
+ var Search = Narou.Search = function(table) {
1013
+ this.initialize(table);
1014
+ };
1015
+
1016
+ Search.get = function(table) {
1017
+ if (!this.__instance) {
1018
+ this.__instance = new this(table);
1019
+ }
1020
+ return this.__instance;
1021
+ };
1022
+
1023
+ $.extend(Search.prototype, {
1024
+ initialize: function(table) {
1025
+ this.table = table;
1026
+ this.myfilter = $("#myFilter");
1027
+ this.filter_tags = [];
1028
+ this.exclusion_tags = []; // 除外タグ
1029
+ this.myfilter_clear = $("#myFilter-clear");
1030
+ this.myfilter_clear_timer_id = null;
1031
+ this.initializeEvents();
1032
+ },
1033
+
1034
+ initializeEvents: function() {
1035
+ var self = this;
1036
+ // フィルターのフックAPIでタグを検索する
1037
+ $.fn.dataTable.ext.search.push(function (settings, data, dataIndex) {
1038
+ if (_.isEmpty(self.filter_tags) && _.isEmpty(self.exclusion_tags))
1039
+ return true;
1040
+
1041
+ var tags = self.table.row(dataIndex).data().tags;
1042
+ var matched = true;
1043
+ // 一致タグ検索
1044
+ _.each(self.filter_tags, function(tag_name) {
1045
+ matched = !!tags.match(new RegExp('data-tag="' + tag_name + '"'));
1046
+ return matched;
1047
+ });
1048
+ if (matched) {
1049
+ // 除外タグ検索
1050
+ _.each(self.exclusion_tags, function(tag_name) {
1051
+ matched = !tags.match(new RegExp('data-tag="' + tag_name + '"'));
1052
+ return matched;
1053
+ });
1054
+ }
1055
+ return matched;
1056
+ });
1057
+
1058
+ // カスタムフィルタボックスの変更イベント
1059
+ this.myfilter.on("keyup", function() {
1060
+ clearTimeout(self.myfilter_clear_timer_id);
1061
+ self.myfilter_clear_timer_id = setTimeout(function() {
1062
+ self.search();
1063
+ }, 300);
1064
+ self.myFilterClearToggleVisiblity();
1065
+ });
1066
+
1067
+ // フィルタのリセットボタン
1068
+ this.myfilter_clear.on("click", function() {
1069
+ self.myfilter.val("");
1070
+ self.myfilter_clear.hide();
1071
+ self.search();
1072
+ });
1073
+
1074
+ // 検索アイコンも反応するようにしておく(コピペとかで反応しない場合用)
1075
+ $("#myFilter-search-icon").on("click", function() {
1076
+ self.search();
1077
+ self.myFilterClearToggleVisiblity();
1078
+ });
1079
+ },
1080
+
1081
+ appendTagToFilter: function(tag_name, exclude) {
1082
+ if (_.includes(this.filter_tags, tag_name) ||
1083
+ _.includes(this.exclusion_tags, tag_name)) {
1084
+ // すでにフィルターに入力済みなら何もしない
1085
+ return;
1086
+ }
1087
+
1088
+ if (tag_name) {
1089
+ var str = sprintf(
1090
+ "%(current)s %(exclude_flag)stag:%(tag_name)s",
1091
+ {
1092
+ current: this.myfilter.val(),
1093
+ exclude_flag: exclude ? "-" : "",
1094
+ tag_name: tag_name
1095
+ });
1096
+ this.myfilter.val(str.trim());
1097
+ }
1098
+ else {
1099
+ this.removeAllTagsByFilter();
1100
+ }
1101
+ this.search();
1102
+ this.myFilterClearToggleVisiblity();
1103
+ },
1104
+
1105
+ removeAllTagsByFilter: function() {
1106
+ var normal_words = this.splitFilter().normal;
1107
+ this.myfilter.val(normal_words.join(" "));
1108
+ this.clearTagCaches();
1109
+ },
1110
+
1111
+ splitFilter: function(string) {
1112
+ string = string || this.myfilter.val().trim();
1113
+ var result = {
1114
+ filter_tags: [], exclusion_tags: []
1115
+ };
1116
+ var words = string.split(/\s+/);
1117
+ result.normal =
1118
+ _.filter(words, function(word) {
1119
+ if (word.match(/^(-?)tag:(.+)$/i)) {
1120
+ var exclude_flag = !!RegExp.$1;
1121
+ var tag_name = RegExp.$2;
1122
+ if (exclude_flag)
1123
+ result.exclusion_tags.push(tag_name);
1124
+ else
1125
+ result.filter_tags.push(tag_name);
1126
+ return false;
1127
+ }
1128
+ else {
1129
+ return true;
1130
+ }
1131
+ }.bind(this));
1132
+ return result;
1133
+ },
1134
+
1135
+ clearTagCaches: function() {
1136
+ this.filter_tags.length = 0;
1137
+ this.exclusion_tags.length = 0;
1138
+ },
1139
+
1140
+ searchSync: function() {
1141
+ this.search(true);
1142
+ },
1143
+
1144
+ _flatPush: function(target, array) {
1145
+ return target.push.apply(target, array);
1146
+ },
1147
+
1148
+ _searchFn: function() {
1149
+ var filter_string = this.myfilter.val().trim();
1150
+ var words = this.splitFilter(filter_string);
1151
+ var normal_words = words.normal;
1152
+
1153
+ this.clearTagCaches();
1154
+ this._flatPush(this.filter_tags, words.filter_tags);
1155
+ this._flatPush(this.exclusion_tags, words.exclusion_tags);
1156
+
1157
+ // タグ以外の単語で通常検索し、タグ部分はフックAPIでフィルターする
1158
+ var normal_words_string = normal_words.join(" ");
1159
+ if (this.before_normal_words_string !== normal_words_string) {
1160
+ // 通常の検索は一度 table.search() を実行すれば維持されるので、
1161
+ // 検索語が変化しないかぎりは一度の実行でいい
1162
+ this.table.search(normal_words_string);
1163
+ this.before_normal_words_string = normal_words_string;
1164
+ }
1165
+ // table.draw() を実行することにより、table.search() のフックAPIが実行される
1166
+ this.table.draw();
1167
+
1168
+ // dataTables は search() に渡された文字列しか自動保存しないので、タグ含めて自前で保存
1169
+ storage.set("filter_string", filter_string).save();
1170
+ },
1171
+
1172
+ search: function(sync) {
1173
+ if (sync)
1174
+ this._searchFn();
1175
+ else
1176
+ setTimeout(this._searchFn.bind(this), 10);
1177
+ },
1178
+
1179
+ myFilterClearToggleVisiblity: function() {
1180
+ if (this.myfilter.val() === "") {
1181
+ this.myfilter_clear.hide();
1182
+ }
1183
+ else {
1184
+ this.myfilter_clear.show();
1185
+ }
1186
+ },
1187
+ });
1188
+
823
1189
  /*************************************************************************
824
1190
  * タグ機能
825
1191
  *************************************************************************/
826
- var Tag = Narou.Tag = (function(table) {
1192
+ var Tag = Narou.Tag = function(table) {
827
1193
  this.table = table;
1194
+ this.search = Search.get(table);
828
1195
  this.registerEvents($("#tag-list-canvas"));
1196
+ this.registerEvents($("#novel-list tbody"));
829
1197
  this.updateCanvas();
830
- });
1198
+ };
831
1199
 
832
1200
  $.extend(Tag.prototype, {
833
1201
  updateCanvas: function() {
@@ -841,18 +1209,17 @@ var Narou = (function() {
841
1209
  var self = this;
842
1210
  if (typeof stop_bubbling === "undefined") stop_bubbling = true;
843
1211
  var args = { stop_bubbling: stop_bubbling };
844
- $target.on("click", ".tag", args, function(e) {
845
- if (e.data.stop_bubbling) e.stopPropagation();
846
- var tag_name = $(this).data("tag");
847
- $("#tag-search").val(tag_name);
848
- storage.set("tag_search", tag_name);
849
- storage.save();
850
- self.table.draw();
851
- self.table.$("[data-toggle=tooltip]").tooltip("hide");
852
- }).on("mousedown", ".tag", args, function(e) {
853
- // 範囲選択モードでもクリック出来るように
854
- if (e.data.stop_bubbling) e.stopPropagation();
855
- });
1212
+ $target
1213
+ .on("click", ".tag", args, function(e) {
1214
+ if (e.data.stop_bubbling) e.stopPropagation();
1215
+ var tag_name = String($(this).data("tag"));
1216
+ self.search.appendTagToFilter(tag_name, e.altKey);
1217
+ self.table.$("[data-toggle=tooltip]").tooltip("hide");
1218
+ })
1219
+ .on("mousedown", ".tag", args, function(e) {
1220
+ // 範囲選択モードでもクリック出来るように
1221
+ if (e.data.stop_bubbling) e.stopPropagation();
1222
+ });
856
1223
  },
857
1224
 
858
1225
  openEditor: function() {
@@ -1114,6 +1481,205 @@ var Narou = (function() {
1114
1481
  },
1115
1482
  });
1116
1483
 
1484
+ /*************************************************************************
1485
+ * 自動保存・同期機能付きメモ帳
1486
+ *************************************************************************/
1487
+ var Notepad = Narou.Notepad = (function() {
1488
+ this.object_id = this.createObjectId();
1489
+ });
1490
+
1491
+ Notepad.replace = function(id, options) {
1492
+ var notepad = new Notepad;
1493
+ notepad.replace(id, options);
1494
+ return notepad;
1495
+ };
1496
+
1497
+ $.extend(Notepad.prototype, {
1498
+ DEFAULTS: {
1499
+ autosave: true,
1500
+ readonly: false,
1501
+ synchronizing: true, // 別ウィンドウ同士で内容を同期するか
1502
+ rows: 20,
1503
+ // string or Deferred オブジェクト
1504
+ text: function() {
1505
+ return $.get("/api/notepad/read");
1506
+ }
1507
+ },
1508
+
1509
+ renderer: _.template(
1510
+ '<div id="<%= container_id %>" class="notepad-container">' +
1511
+ '<textarea class="form-control" rows="<%- rows %>" ' +
1512
+ '<% if (readonly) { %>readonly<% } %>' +
1513
+ '><%- text %></textarea>' +
1514
+ '<span class="notepad-icon glyphicon glyphicon-ok text-success hide"></span>' +
1515
+ '</div>'
1516
+ ),
1517
+
1518
+ replace: function(id, options) {
1519
+ var opt = _.merge({}, this.DEFAULTS, options);
1520
+ this.id = id;
1521
+ this.autosave = opt.autosave;
1522
+ this.readonly = opt.readonly;
1523
+ this.synchronizing = opt.synchronizing;
1524
+ this.rows = opt.rows;
1525
+ this.text = opt.text;
1526
+
1527
+ this.createElements();
1528
+ },
1529
+
1530
+ save: function(textarea) {
1531
+ var self = this;
1532
+ textarea._old_value = textarea.value;
1533
+ return $.post("/api/notepad/save", {
1534
+ text: textarea.value,
1535
+ object_id: this.object_id
1536
+ })
1537
+ .done(function() {
1538
+ self.activeOkIcon();
1539
+ });
1540
+ },
1541
+
1542
+ createObjectId: function() {
1543
+ return String(_.now()) + _.random(0, 10000);
1544
+ },
1545
+
1546
+ createElements: function() {
1547
+ var self = this;
1548
+ var text = this.text;
1549
+ var render = function(stringified_text) {
1550
+ var rendered_html = self.renderer({
1551
+ container_id: self.containerId(),
1552
+ readonly: self.readonly,
1553
+ rows: self.rows,
1554
+ text: stringified_text
1555
+ });
1556
+ var elm = $("#" + self.id).html(rendered_html);
1557
+ var textarea = elm.find("textarea");
1558
+ if (self.autosave && !self.readonly) {
1559
+ self.attachAutoSaveEvents(textarea);
1560
+ }
1561
+ if (self.synchronizing) {
1562
+ self.attachSynchronizingEvents(textarea);
1563
+ }
1564
+ };
1565
+
1566
+ if (typeof text == "function") {
1567
+ text()
1568
+ .done(function(stringified_text) {
1569
+ render(stringified_text);
1570
+ })
1571
+ .fail(function() {
1572
+ render("");
1573
+ });
1574
+ }
1575
+ else {
1576
+ render(text);
1577
+ }
1578
+ },
1579
+
1580
+ attachAutoSaveEvents: function(textarea) {
1581
+ var timer_id = null;
1582
+ var self = this;
1583
+
1584
+ textarea
1585
+ .on("focus", function() {
1586
+ this._old_value = this.value;
1587
+ })
1588
+ .on("blur", function() {
1589
+ if (this.value !== this._old_value) {
1590
+ self.save(this);
1591
+ }
1592
+ })
1593
+ .on("keyup paste cut", function() {
1594
+ // paste, cut イベントは実行される「直前」に発生するので、
1595
+ // 実際にテキストボックスに反映されるまでに少し待つ
1596
+ setTimeout(function() {
1597
+ clearTimeout(timer_id);
1598
+ timer_id = setTimeout(function(value, old_value) {
1599
+ if (value !== old_value) {
1600
+ self.save(this);
1601
+ }
1602
+ }.bind(this), 1000, this.value, this._old_value);
1603
+ }.bind(this), 10);
1604
+ });
1605
+ },
1606
+
1607
+ activeOkIcon: function() {
1608
+ var icon = this.container().find(".notepad-icon");
1609
+ icon
1610
+ .removeClass("hide")
1611
+ .show()
1612
+ .delay(2000)
1613
+ .fadeOut(1000);
1614
+ },
1615
+
1616
+ attachSynchronizingEvents: function(textarea) {
1617
+ var self = this;
1618
+ var notification = Notification.instance();
1619
+ notification.on("notepad.change", function(data) {
1620
+ if (data.object_id == self.object_id)
1621
+ return;
1622
+ var dom = textarea[0];
1623
+ if (dom.value != data.text) {
1624
+ dom.value = dom._old_value = data.text;
1625
+ }
1626
+ });
1627
+ },
1628
+
1629
+ container: function() {
1630
+ return $("#" + this.containerId());
1631
+ },
1632
+
1633
+ containerId: function() {
1634
+ return this.id + "_notepad";
1635
+ },
1636
+ });
1637
+
1638
+ /*************************************************************************
1639
+ * 埋め込みテンプレート変換
1640
+ *************************************************************************/
1641
+ Narou.Template = {
1642
+ // role="template" を探して、そのテンプレートを処理したあと
1643
+ // 同じ場所にレンダリング結果を埋め込む。
1644
+ // 一回だけレンダリングすればいいもの向け
1645
+ replaceAll: function(hash) {
1646
+ $("[role=template]").each(function() {
1647
+ var renderer = _.template($(this).text());
1648
+ $(this).after(renderer(hash));
1649
+ });
1650
+ }
1651
+ };
1652
+
1653
+ /*************************************************************************
1654
+ * フラッシュメッセージ
1655
+ *************************************************************************/
1656
+ Narou.Flash = {
1657
+ renderer: _.template(
1658
+ '<div class="container">' +
1659
+ '<div class="fadeout-alert alert alert-<%= type %>">' +
1660
+ '<%= message %>' +
1661
+ '</div></div>'
1662
+ ),
1663
+
1664
+ show: function(message, type) {
1665
+ var obj = $(Narou.Flash.renderer({
1666
+ message: message, type: type || "success"
1667
+ }));
1668
+ $("body").append(obj);
1669
+ this.setEvents(obj);
1670
+ },
1671
+
1672
+ setEvents: function(object) {
1673
+ $(object)
1674
+ .delay(2000)
1675
+ .animate({ opacity: "hide" }, 1500)
1676
+ .queue(function(next) {
1677
+ $(this).remove();
1678
+ next();
1679
+ });
1680
+ }
1681
+ };
1682
+
1117
1683
  return Narou;
1118
1684
  })();
1119
1685