/* -*- coding: utf-8 -*- * * Copyright 2013 whiteleaf. All rights reserved. */ var Narou = (function() { "use strict"; var Narou = {}; var storage_cache = null; var storage = null; /************************************************************************* * ローカルストレージ *************************************************************************/ var Storage = Narou.Storage = function() { this.initialize(); }; $.extend(Storage.prototype, { storage_name: "Narou.rb_WEB_UI_saved", initialize: function() { this.objects = storage_cache ? storage_cache : this.load(); }, load: function() { var objects = localStorage.getItem(this.storage_name); return objects ? JSON.parse(objects) : {}; }, save: function() { localStorage.setItem(this.storage_name, JSON.stringify(this.objects)); }, get: function(key) { return this.objects[key]; }, set: function(key, value) { this.objects[key] = value; return this; }, }); storage = new Storage(); /************************************************************************* * ユーティリティ *************************************************************************/ $.extend(Narou, { registerCloseHandler: function(callback) { // Chrome, IEですぐにclickイベントをバインドすると、メニュー表示時の // クリックに反応してしまう(表示上のズレによって、クリック時のマウス // 座標上に対象オブジェクトが存在しないため)ので、イベント作成をほんの // 少し遅らせる setTimeout(function() { // 関係ないところをクリックした時に閉じる $(document).one("click", callback); }, 100); }, popupMenu: function(menu_id, pos, close_menu_handler) { var $menu = $(menu_id); var left = $(window).width() < pos.x - $(document).scrollLeft() + $menu.outerWidth() ? pos.x - $menu.outerWidth() : pos.x; var top = $(window).height() < pos.y - $(document).scrollTop() + $menu.outerHeight() ? pos.y - $menu.outerHeight() : pos.y; $menu.show().offset({ left: left, top: top }); Narou.registerCloseHandler(close_menu_handler); }, // http://qiita.com/osakanafish/items/c64fe8a34e7221e811d0 formatDate: function(date, format) { if (!format) format = 'YYYY-MM-DD hh:mm:ss.SSS'; format = format.replace(/YYYY/g, date.getFullYear()); format = format.replace(/MM/g, ('0' + (date.getMonth() + 1)).slice(-2)); format = format.replace(/DD/g, ('0' + date.getDate()).slice(-2)); format = format.replace(/hh/g, ('0' + date.getHours()).slice(-2)); format = format.replace(/mm/g, ('0' + date.getMinutes()).slice(-2)); format = format.replace(/ss/g, ('0' + date.getSeconds()).slice(-2)); if (format.match(/S/g)) { var milliSeconds = ('00' + date.getMilliseconds()).slice(-3); var length = format.match(/S/g).length; for (var i = 0; i < length; i++) format = format.replace(/S/, milliSeconds.substring(i, i + 1)); } return format; }, }); /************************************************************************* * Push 通知管理 *************************************************************************/ var Notification = Narou.Notification = function() { this.initialize(); }; Notification.instance = function() { if (!this.__instance) { this.__instance = new this; } return this.__instance; }; $.extend(Notification.prototype, { RETRY_LIMIT: 5, RETRY_WAIT: 2000, initialize: function() { this.events = {}; this.retry_count = this.RETRY_LIMIT; this.connect(); }, connect: function() { if (this.connection) return; var self = this; this.connected = false; var connection = window.c = this.connection = new WebSocket(this.create_ws_uri()); connection.onopen = function() { self.connected = true; self.trigger("console.clear"); self.retry_count = self.RETRY_LIMIT; // 接続出来たらリトライカウント回復 }; connection.onclose = function() { self.connection = null; // PCのスリープ等でコネクションが切れた場合に再接続する if (self.retry_count-- > 0) { setTimeout(function() { self.connected = false; self.connect(); }, self.RETRY_WAIT); } }; connection.onmessage = function(e) { if (e && e.data) { self.onmessage(JSON.parse(e.data)); } }; }, create_ws_uri: function() { var host = location.hostname, port = location.port; return "ws://" + host + ":" + (parseInt(port) + 1) + "/"; }, onmessage: function(data) { var self = this; $.each(data, function(event, value) { self.trigger(event, value); }); }, on: function(event, block) { if (typeof block !== "function") { $.error("need a function"); } var stack = this.events[event] || []; stack.push(block); this.events[event] = stack; }, trigger: function(event, data) { var self = this; var stack = this.events[event] || []; $.each(stack, function() { this.call(self, data); }); }, send: function(json) { this.connection.send(JSON.stringify(json)); }, }); /************************************************************************* * コンテキストメニュー *************************************************************************/ var ContextMenu = Narou.ContextMenu = function(action, notification) { this.action = action; this.notification = notification; this.closed = true; this.initializeConsoleDialog(); this.initializeMenuEvent(); }; $.extend(ContextMenu.prototype, { open: function(target_id, pos, callback) { this.target_id = target_id; if (!this.closed) { // メニューを開いた状態で直接ボタンを押した場合に一旦閉じるイベントを起こさせる $(document).trigger("click"); } this.closed = false; Narou.popupMenu("#context-menu", pos, function() { $("#context-menu").hide(); if (typeof callback === "function") { callback(); } }); }, openConsoleDialog: function(callback) { if (typeof callback !== "function") return; var $console_dialog = $("#console-dialog"); $console_dialog.one("show.bs.modal", callback); $(document).one("cancel.narou.remove", function() { $console_dialog.modal("hide"); }); this.console.clear(); $console_dialog.modal(); }, initializeConsoleDialog: function() { this.console = new Narou.Console(this.notification, { restore: false, buttons: false, id: "#each-console" }); }, initializeMenuEvent: function() { var $context_menu = $("#context-menu"); var self = this; $("#context-menu-setting").on("click", function(e) { e.preventDefault(); location.href = "/novels/" + self.target_id + "/setting"; }); $("#context-menu-update").on("click", function(e) { e.preventDefault(); self.openConsoleDialog(function() { self.action.update(self.target_id); }); }); $("#context-menu-send").on("click", function(e) { e.preventDefault(); self.openConsoleDialog(function() { self.action.send(self.target_id); }); }); $("#context-menu-freeze-toggle").on("click", function(e) { e.preventDefault(); self.action.freeze(self.target_id); }); $("#context-menu-remove").on("click", function(e) { e.preventDefault(); self.openConsoleDialog(function() { self.action.remove(self.target_id); }); }); $("#context-menu-remove-with-file").on("click", function(e) { e.preventDefault(); self.openConsoleDialog(function() { self.action.removeWithFile(self.target_id); }); }); $("#context-menu-convert").on("click", function(e) { e.preventDefault(); self.openConsoleDialog(function() { self.action.convert(self.target_id); }); }); $("#context-menu-diff").on("click", function(e) { e.preventDefault(); self.action.diff(self.target_id); }); $("#context-menu-inspect").on("click", function(e) { e.preventDefault(); self.openConsoleDialog(function() { self.action.inspect(self.target_id); }); }); $("#context-menu-folder").on("click", function(e) { e.preventDefault(); self.action.folder(self.target_id); }); $("#context-menu-backup").on("click", function(e) { e.preventDefault(); self.openConsoleDialog(function() { self.action.backup(self.target_id); }); }); } }); /************************************************************************* * アクション *************************************************************************/ var Action = Narou.Action = function(table) { this.table = table; }; $.extend(Action.prototype, { _getSelectedIds: function(args) { if (typeof args !== "undefined" && args.length > 0) { return Array.prototype.slice.call(args); } var ids = []; $.each(this.table.rows(".selected").data(), function(i, val) { ids.push(val.id); }); return ids; }, selectAll: function() { this.table.$("tr").addClass("selected"); this.table.fireChangeSelect(); }, selectView: function() { $("#novel-list tbody tr").addClass("selected"); this.table.fireChangeSelect(); }, selectClear: function() { this.table.$("tr.selected").removeClass("selected"); this.table.fireChangeSelect(); }, download: function() { bootbox.prompt("ダウンロードする小説のURL、もしくはNコードを入力", function(target) { if (!target) return; $.post("/api/download", { "target": target }); console.log("new downloading %o", target); }); }, downloadForce: function() { var ids = this._getSelectedIds(arguments); if (ids.length === 0) return; $.post("/api/download_force", { "ids": ids }); console.log("force downloading " + ids.join(", ")); }, update: function() { var ids = this._getSelectedIds(arguments); if (ids.length === 0) { $.post("/api/update"); console.log("updating all"); } else { $.post("/api/update_select", { "ids": ids }); console.log("updating " + ids.join(", ")); } }, send: function() { var ids = this._getSelectedIds(arguments); if (ids.length === 0) { $.post("/api/send"); console.log("sending all"); } else { $.post("/api/send_select", { "ids": ids }); console.log("sending " + ids.join(", ")); } }, freeze: function() { var ids = this._getSelectedIds(arguments); if (ids.length === 0) return; $.post("/api/freeze", { "ids": ids }); console.log("freezing(toggle) " + ids.join(", ")); }, freezeOn: function() { var ids = this._getSelectedIds(arguments); if (ids.length === 0) return; $.post("/api/freeze_on", { "ids": ids }); console.log("freezing " + ids.join(", ")); }, freezeOff: function() { var ids = this._getSelectedIds(arguments); if (ids.length === 0) return; $.post("/api/freeze_off", { "ids": ids }); console.log("thawing " + ids.join(", ")); }, _removeConfirmDialog: function(title, ids, callback) { var message = ""; this.table.rows().data().each(function(data, idx) { if (ids.indexOf(data.id + "") !== -1) { message += "