var PLUGIN_INFO = Stella すてら Show video informations on the status line. ステータスラインに動画の再生時間などを表示する。 0.09 anekos 2.0pre 2.0pre Creative Commons Attribution-Share Alike 3.0 Unported : set to the specified volume. :stmu[te]: turn on/off mute. :stre[peat]: turn on/off mute. :stco[mment]: turn on/off comment visible. :stse[ek] : seek to specified position. TIMECODE formats :stseek 1:30 # 1分30秒 :stseek 1.5 # 1.5分。90秒 :stseek 90 # 90秒 :stse[ek]! : seek to the specified position from current position at relatively. :stfe[tch]: fetch and save the video. :stla[rge]: enlarge video screen. :stfu[llscreen]: turn on/off fullscreen. ]]> : 指定の音量にする。 0から100の数字で指定する。 :stmu[te]: ミュートのOn/Offを切り替える。 :stre[peat]: リピートモードのOn/Offを切り替える。 :stco[mment]: コメントのOn/Offを切り替える。 :stse[ek] : 指定の秒数までシークスする。 TIMECODE は以下の様に指定できる。 :stseek 1:30 # 1分30秒 :stseek 1.5 # 1.5分。90秒 :stseek 90 # 90秒 :stse[ek]! : 現在の位置から TIMECODE 分移動する。 :stfe[tch]: 動画をファイルとして保存する。 :stla[rge]: 画面を大きくする/戻す。 :stfu[llscreen]: フルスクリーン表示のOn/Offを切り替える。 ]]> ; // TODO // Icons // Other video hosting websites // auto fullscreen // // Link: // http://d.hatena.ne.jp/nokturnalmortum/ // // Refs: // http://yuichis.homeip.net/nicodai.user.html // http://coderepos.org/share/browser/lang/javascript/vimperator-plugins/trunk/nicontroller.js // http://coderepos.org/share/browser/lang/javascript/vimperator-plugins/trunk/youtubeamp.js // // Thanks: // 参考にさせてもらった人々。THANKS!! // janus_wel 氏 // http://d.hatena.ne.jp/janus_wel/ // ゆういち 氏 // http://yuichis.homeip.net/nicodai.user.html (function () { /********************************************************************************* * Const {{{ *********************************************************************************/ const ID_PREFIX = 'anekos-stela-'; const InVimperator = !!(liberator && modules && modules.liberator); const DOUBLE_CLICK_INTERVAL = 300; // }}} /********************************************************************************* * Utils {{{ *********************************************************************************/ function bindr (_this, f) function () f.apply(_this, arguments); function capitalize (s) s.replace(/^[a-z]/, String.toUpperCase).replace(/-[a-z]/, function (s) s.slice(1).toUpperCase()); function currentURL () content.document.location.href; function download (url, filepath, ext, title) { let dm = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager); let wbp = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"].createInstance(Ci.nsIWebBrowserPersist); let file; if (filepath) { file = io.getFile(io.expandPath(filepath)); } else { file = dm.userDownloadsDirectory; } if (file.isDirectory() && title) file.appendRelativePath(fixFilename(title) + ext); if (file.exists()) return liberator.echoerr('The file already exists! -> ' + file.path); file = makeFileURI(file); let dl = dm.addDownload(0, makeURL(url, null, null), file, title, null, null, null, null, wbp); wbp.progressListener = dl; wbp.persistFlags |= wbp.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION; wbp.saveURI(makeURL(url), null, null, null, null, file); return true; } function fixFilename (filename) { const badChars = /[\\\/:;*?"<>|]/g; return filename.replace(badChars, '_'); } function httpRequest (uri, onComplete) { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function () { if (xhr.readyState == 4) { if (xhr.status == 200) onComplete && onComplete(xhr); else raise(xhr.statusText); } }; xhr.open('GET', uri, !!onComplete); xhr.send(null); return xhr; } function id (value) value; function isNum (v) (typeof v === 'number' && !isNaN(v)); function lz (s,n) String(Math.pow(10,n ) + s).substring(1); function makeFile (s) { var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); file.initWithPath(s); return file; } function makeURL (s) { let url = Cc["@mozilla.org/network/standard-url;1"].createInstance(Ci.nsIURL); url.spec = s; return url; } let raise = InVimperator ? function (error) {throw new Error(error)} : function (error) liberator.echoerr(error); function toTimeCode(v) (isNum(v) ? (parseInt((v / 60)) + ':' + lz(v % 60, 2)) : '??:??'); function storeStyle (target, values, overwrite) { let [style, cstyle] = [target.style, content.getComputedStyle(target, '')]; let backup = {}; for (let [name, value] in Iterator(values)) { backup[name] = cstyle[name]; style[name] = value; } if(overwrite || !style.__stella_backup) style.__stella_backup = backup; } function restoreStyle (target, doDelete) { let style = target.style; if (!style.__stella_backup) return; let backup = style.__stella_backup; for (let name in Iterator(backup)) style[name] = backup[name]; if (doDelete) delete style.__stella_backup; } function getElementByIdEx (id) let (p = content.document.getElementById(id)) (p && (p.wrappedJSObject || p)); function fixDoubleClick (obj, click, dblClick) { let clicked = 0; let original = {click: obj[click], dblClick: obj[dblClick]}; obj[click] = function () { let self = this, args = arguments; let _clicked = ++clicked; setTimeout(function () { if (_clicked == clicked--) original.click.apply(self, args); else clicked = 0; }, DOUBLE_CLICK_INTERVAL); }; obj[dblClick] = function () { clicked = 0; original.dblClick.apply(this, arguments); }; } // 上手い具合に病数に直すよ function fromTimeCode (code) { let m; function sign (s, v) (s == '-' ? -v : v); if (typeof code != 'number') { code = code.toString(); if (m = code.match(/^([-+])?(\d+):(\d+)$/)) return sign(m[1], parseInt(m[2]) * 60 + parseInt(m[3])); if (m = code.match(/^([-+])?(\d+.\d+)$/)) return sign(m[1], parseFloat(m[2]) * 60); } return parseInt(code); } // }}} /********************************************************************************* * Player {{{ *********************************************************************************/ function Player () { let self = this; this.initialize.apply(this, arguments); function setf (name, value) ((self.functions[name] === undefined) && (self.functions[name] = value || '')); let (seek = this.has('currentTime', 'rw', 'totalTime', 'r') && 'x') { setf('seek', seek); setf('seekRelative', seek); } setf('playOrPause', this.has('play', 'x', 'pause', 'x') && 'x'); setf('turnUpDownVolume', this.has('volume', 'rw') && 'x'); setf('maxVolume', this.has('volume', 'rw') && 'r'); setf('fetch', this.has('fileURL', 'r') && 'x'); setf('relations', [name for each (name in Player.RELATIONS) if (this.has(name, 'r'))].length && 'r'); if (!this.functions.large) this.functions.large = this.functions.fullscreen; } Player.ST_PLAYING = 'playing'; Player.ST_PAUSED = 'paused'; Player.ST_ENDED = 'ended'; Player.ST_OTHER = 'other'; Player.REL_TAG = 'tag'; Player.REL_ID = 'id'; Player.REL_SEARCH = 'search'; Player.REL_URL = 'url'; Player.RELATIONS = { REL_TAG: 'relatedTags', REL_ID: 'relatedIDs' }; // rwxt で機能の有無を表す // r = read // w = write // x = function // t = toggle Player.prototype = { functions: { currentTime: '', fileExtension: 'r', fileURL: '', fullscreen: '', makeURL: '', muted: '', pause: '', play: '', playEx: '', relatedIDs: '', relatedTags: '', repeating: '', large: '', tags: '', title: '', totalTime: '', volume: '', // auto setting => fetch maxVolume playOrPause relations seek seekRelative turnUpDownVolume }, icon: null, initialize: function () void null, finalize: function () { // 念のためフルスクリーンは解除しておく if (this.has('fullscreen', 'rwt') && this.isValid && this.fullscreen) this.fullscreen = false; }, is: function (state) (this.state == state), has: function (name, ms) (arguments.length < 2) || let (f = this.functions[name]) (f && !Array.some(ms, function (m) f.indexOf(m) < 0)) && arguments.callee.apply(this, Array.splice(arguments, 2)), get currentTime () undefined, set currentTime (value) value, get fileExtension () '', get fullscreen () undefined, set fullscreen (value) value, get fileURL () undefined, get maxVolume () 100, get muted () undefined, set muted (value) value, // [{name: ..., title: ...}, ...] get relatedIDs () undefined, // [{type: ..., value: ...}, ...] get relations () { if (!this.has('relations', 'r')) return []; let result = []; for (let [type, name] in Iterator(Player.RELATIONS)) { if (this.has(name, 'r')) { try{ result = this[name].map(function (it) ({type: Player[type], value: it})).concat(result); } catch (e){ liberator.log(name); } } } return result; }, get repeating () undefined, set repeating (value) value, get large () this.fullscreen, set large (value) (this.fullscreen = value), get state () undefined, get statusText () this.timeCodes, get storage () (content.document.__stella_storage || (content.document.__stella_storage = {})), get timeCodes () (toTimeCode(this.currentTime) + '/' + toTimeCode(this.totalTime)), get title () undefined, get isValid () (~buffer.URL.indexOf('http://www.nicovideo.jp/watch/')), get volume () undefined, set volume (value) value, fetch: function (filepath) download(this.fileURL, filepath, this.fileExtension, this.title), makeURL: function () undefined, pause: function () undefined, play: function () undefined, playEx: function () { if (this.is(Player.ST_ENDED)) this.currentTime = 0; this.play(); }, playOrPause: function () { if (this.is(Player.ST_PLAYING)) { this.pause(); } else { this.playEx(); } }, seek: function (v) { v = fromTimeCode(v); if (v < 0) v = this.totalTime + v; return this.currentTime = Math.min(Math.max(v, 0), this.totalTime); }, seekRelative: function (v) this.currentTime = Math.min(Math.max(this.currentTime + fromTimeCode(v), 0), this.totalTime), toggle: function (name) { if (!this.has(name, 'rwt')) return; let v = this[name]; this[name] = !v; return !v; }, turnUpDownVolume: function (v) this.volume = Math.min(Math.max(this.volume + parseInt(v), 0), this.maxVolume) }; // }}} /********************************************************************************* * YouTubePlayer {{{ *********************************************************************************/ function YouTubePlayer () { Player.apply(this, arguments); } YouTubePlayer.prototype = { __proto__: Player.prototype, functions: { currentTime: 'rw', fileURL: 'r', fullscreen: 'rwt', muted: 'rwt', pause: 'x', play: 'x', playEx: 'x', playOrPause: 'x', repeating: 'rw', title: 'r', totalTime: 'r', volume: 'rw' }, icon: 'http://www.youtube.com/favicon.ico', get currentTime () parseInt(this.player.getCurrentTime()), set currentTime (value) (this.player.seekTo(fromTimeCode(value)), this.currentTime), get fileExtension () '.mp4', get fileURL () let (as = content.document.defaultView.wrappedJSObject.swfArgs) ('http://www.youtube.com/get_video?fmt=22&video_id=' + as.video_id + '&t=' + as.t), get fullscreen () this.storage.fullscreen, // FIXME - 元に戻らない&スクロールバーが出たまま set fullscreen () { this.storage.fullscreen = !this.storage.fullscreen; let p = this.player; let r = p.getBoundingClientRect(); if (this.fullscreen) { if (this.storage.r === undefined) this.storage.r = /r/.test(options['guioptions']); storeStyle(p, { marginLeft: -r.left + 'px', marginTop: -r.top + 'px', width: content.innerWidth + 'px', height: content.innerHeight + 'px', }); p.setSize(content.innerWidth, content.innerHeight); } else { p.setSize(640, 385); restoreStyle(p); } }, get muted () this.player.isMuted(), set muted (value) ((value ? this.player.mute() : this.player.unMute()), value), get player () let (p = content.document.getElementById('movie_player')) (p && (p.wrappedJSObject || p)), get state () { switch (this.player.getPlayerState()) { case 0: return Player.ST_ENDED; case 1: return Player.ST_PLAYING; case 2: return Player.ST_PAUSED; case 3: // buffering case 5: //video cued case -1: //unstarted default: return Player.ST_OTHER; } }, get title () content.document.title.replace(/^YouTube - /, ''), get totalTime () parseInt(this.player.getDuration()), get isValid () buffer.URL.match(/^http:\/\/(?:[^.]+\.)?youtube\.com\/watch/), get volume () parseInt(this.player.getVolume()), set volume (value) (this.player.setVolume(value), this.volume), play: function () this.player.playVideo(), pause: function () this.player.pauseVideo() }; // }}} /********************************************************************************* * NicoPlayer {{{ *********************************************************************************/ function NicoPlayer () { Player.apply(this, arguments); } // Normal / Fullscreen NicoPlayer.Variables = [ ['videowindow._xscale', 100, null], ['videowindow._yscale', 100, null], ['videowindow._x', 6, 0], ['videowindow._y', 65, 0], ['controller._x', 6, -1000], ['inputArea._x', 4, -1000], ['controller._visible', 1, 1], ['inputArea._visible', 1, 1], ['waku._visible', 1, 0], ['tabmenu._visible', 1, 0], ['videowindow.video_mc.video.smoothing', null, 1], ['videowindow.video_mc.video.deblocking', null, 5] ]; NicoPlayer.SIZE_NORMAL = 'normal'; NicoPlayer.SIZE_LARGE = 'fit'; NicoPlayer.prototype = { __proto__: Player.prototype, functions: { comment: 'rwt', currentTime: 'rw', fetch: 'x', fileURL: '', fullscreen: 'rwt', id: 'r', makeURL: 'x', muted: 'rwt', large: 'rwt', pause: 'x', play: 'x', playEx: 'x', playOrPause: 'x', relatedIDs: 'r', relatedTags: 'r', repeating: 'rwt', tags: 'r', title: 'r', totalTime: 'r', volume: 'rw' }, icon: 'http://www.nicovideo.jp/favicon.ico', get comment () this.player.ext_isCommentVisible(), set comment (value) (this.player.ext_setCommentVisible(value), value), get playerContainer () getElementByIdEx('flvplayer_container'), get currentTime () parseInt(this.player.ext_getPlayheadTime()), set currentTime (value) (this.player.ext_setPlayheadTime(fromTimeCode(value)), this.currentTime), get fileExtension () '.flv', get fullscreen () !!this.storage.fullscreen, set fullscreen (value) { value = !!value; if (this.storage.fullscreen === value) return; this.storage.fullscreen = value; let self = this, player = this.player, win = content.wrappedJSObject, doc = content.document.wrappedJSObject; if (player.ext_getVideoSize() === 'fit') player.ext_setVideoSize('normal'); win.toggleMaximizePlayer(); if(value) { turnOn(); win.onresize = fixFullscreen; } else { turnOff(); delete win.onresize; } win.scrollTo(0, 0); // 以下関数定義のみ - setVariables turnOn/Off fixFullscreen function setVariables (fullscreen) { NicoPlayer.Variables.forEach(function ([name, normal, full]) { let v = fullscreen ? full : normal; if (v !== null) self.player.SetVariable(name, v); }); } function turnOn () { let viewer = {w: 544, h: 384}; let screen = { w: content.innerWidth, h: content.innerHeight }; let scale = { w: Math.max(1, screen.w / viewer.w), h: Math.max(1, screen.h / viewer.h) }; scale.v = Math.min(scale.w, scale.h); storeStyle(doc.body, { backgroundImage: 'url()', backgroundRepeat: '', backgroundColor: 'black' }); storeStyle( player, (scale.w >= scale.h) ? { width: Math.floor(viewer.w * scale.h) + 'px', height: screen.h + 'px', marginLeft: ((screen.w - viewer.w * scale.h) / 2) + 'px', marginTop: '0px' } : { width: screen.w + 'px', height: Math.floor(viewer.h * scale.w) + 'px', marginLeft: '0px', marginTop: ((screen.h - viewer.h * scale.w) / 2) + 'px' } ); player.SetVariable('videowindow._xscale', 100 * scale.v); player.SetVariable('videowindow._yscale', 100 * scale.v); setVariables(true); } function turnOff () { restoreStyle(doc.body, true); restoreStyle(player, true); player.style.marginLeft = ''; player.style.marginTop = ''; setVariables(false); } function fixFullscreen () ((InVimperator && liberator.mode === modes.COMMAND_LINE) || setTimeout(turnOn, 500)); }, get id () let (m = currentURL().match(/\/watch\/([a-z]{2}\d+)/)) (m && m[1]), get muted () this.player.ext_isMute(), set muted (value) (this.player.ext_setMute(value), value), get player () getElementByIdEx('flvplayer'), get relatedIDs () { if (this.__rid_last_url == currentURL()) return this.__rid_cache || []; this.__rid_last_url = currentURL(); let videos = []; let uri = 'http://www.nicovideo.jp/api/getrelation?sort=p&order=d&video=' + this.id; let xhr = new XMLHttpRequest(); xhr.open('GET', uri, false); xhr.send(null); let xml = xhr.responseXML; let v, vs = xml.evaluate('//video', xml, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE , null); while (v = vs.iterateNext()) { let [cs, video] = [v.childNodes, {}]; for each (let c in cs) if (c.nodeName != '#text') video[c.nodeName] = c.textContent; videos.push({ title: video.title, id: video.url.replace(/^.+?\/watch\//, ''), raw: video }); } return this.__rid_cache = videos; }, get relatedTags() { let nodes = content.document.getElementsByClassName('nicopedia'); return [it.textContent for each (it in nodes) if (it.rel == 'tag')]; }, get repeating () this.player.ext_isRepeat(), set repeating (value) (this.player.ext_setRepeat(value), value), get large () this.player.ext_getVideoSize() === NicoPlayer.SIZE_LARGE, set large (value) { liberator.log(value) this.player.ext_setVideoSize(value ? NicoPlayer.SIZE_LARGE : NicoPlayer.SIZE_NORMAL) return this.large; }, get state () { switch (this.player.ext_getStatus()) { case 'end': return Player.ST_ENDED; case 'playing': return Player.ST_PLAYING; case 'paused': return Player.ST_PAUSED; case 'buffering': default: return Player.ST_OTHER; } }, get title () content.document.title.replace(/\s*\u2010\s*\u30CB\u30B3\u30CB\u30B3\u52D5\u753B(.+)$/, ''), get totalTime () parseInt(this.player.ext_getTotalTime()), get volume () parseInt(this.player.ext_getVolume()), set volume (value) (this.player.ext_setVolume(value), this.volume), fetch: function (filepath) { let onComplete = bindr(this, function (xhr) { let res = xhr.responseText; let info = {}; res.split(/&/).forEach(function (it) let ([n, v] = it.split(/=/)) (info[n] = v)); download(decodeURIComponent(info.url), filepath, this.fileExtension, this.title); }); httpRequest('http://www.nicovideo.jp/api/getflv?v=' + this.id, onComplete); }, makeURL: function (value, type) { switch (type) { case Player.REL_ID: return 'http://www.nicovideo.jp/watch/' + value; case Player.REL_TAG: return 'http://www.nicovideo.jp/tag/' + encodeURIComponent(value); case Player.REL_SEARCH: return 'http://www.nicovideo.jp/search/' + encodeURIComponent(value); } return value; }, pause: function () this.player.ext_play(false), play: function () this.player.ext_play(true), playOrPause: function () { if (this.is(Player.ST_PLAYING)) { this.pause(); } else { let base = this.currentTime; setTimeout(bindr(this, function () (base === this.currentTime ? this.playEx() : this.pause())), 100); } }, }; // }}} /********************************************************************************* * ContextMenu {{{ *********************************************************************************/ const ContextMenuVolume = []; for (let i = 0; i <= 100; i += 10) ContextMenuVolume.push({name: 'setVolume', label: i + '%', attributes: {volume: i}}); const ContextMenuTree = [ 'play', 'pause', 'comment', 'repeat', 'fullscreen', 'fetch', { name: 'volume-root', label: 'Volume', id: ID_PREFIX + 'volume-menupopup', sub: ContextMenuVolume }, { name: 'relations-root', label: 'Relations', id: ID_PREFIX + 'relations-menupopup', sub: [] } ]; function buildContextMenu (setting) { function append (parent, menu) { if (typeof menu == 'string') menu = {name: menu}; if (menu instanceof Array) return menu.forEach(function (it) append(parent, it)); if (!menu.label) menu.label = capitalize(menu.name); let (elem) { if (menu.sub) { let _menu = document.createElement('menu'); let _menupopup = elem = document.createElement('menupopup'); _menu.setAttribute('label', menu.label); _menu.appendChild(_menupopup); parent.appendChild(_menu); append(_menupopup, menu.sub); } else { elem = document.createElement('menuitem'); elem.setAttribute('label', menu.label); parent.appendChild(elem); } menu.id && elem.setAttribute('id', menu.id); for (let [name, value] in Iterator(menu.attributes || {})) elem.setAttribute(name, value); setting.onAppend.call(setting, elem, menu); } } let root = document.createElement('menupopup'); root.id = setting.id; append(root, setting.tree); setting.set.setAttribute('context', root.id); setting.parent.appendChild(root); return root; } // }}} /********************************************************************************* * Event {{{ *********************************************************************************/ function WebProgressListener (listeners) { let self = this; for (let [name, listener] in Iterator(listeners)) this[name] = listener; getBrowser().addProgressListener(this); // これは必要? window.addEventListener('unload', bindr(this.uninstall), false); } WebProgressListener.prototype = { onStatusChange: function (webProgress, request, stateFlags, staus) undefined, onProgressChange: function (webProgress, request, curSelfProgress, maxSelfProgress, curTotalProgress, maxTotalProgress) undefined, onLocationChange: function (webProgress, request, location) undefined, onStateChange: function(webProgress, request, status, message) undefined, onSecurityChange: function(webProgress, request, state) undefined, uninstall: function () { getBrowser().removeProgressListener(this); }, }; // }}} /********************************************************************************* * Stella {{{ *********************************************************************************/ function Stella () { this.initialize.apply(this, arguments); } Stella.MAIN_PANEL_ID = ID_PREFIX + 'main-panel', Stella.MAIN_MENU_ID = ID_PREFIX + 'main-menu', Stella.VOLUME_MENU_ID = ID_PREFIX + 'volume-menu', Stella.prototype = { // new 時に呼ばれる initialize: function () { let self = this; this.players = { niconico: new NicoPlayer(), youtube: new YouTubePlayer() }; this.createStatusPanel(); this.onLocationChange(); this.__onResize = window.addEventListener('resize', bindr(this, this.onResize), false); this.progressListener = new WebProgressListener({onLocationChange: bindr(this, this.onLocationChange)}); }, // もちろん、勝手に呼ばれたりはしない。 finalize: function () { this.removeStatusPanel(); this.disable(); this.progressListener.uninstall(); for each (let player in this.players) player.finalize(); window.removeEventListener('resize', this.__onResize, false); }, get hidden () (this.panel.hidden), set hidden (v) (this.panel.hidden = v), get isValid () (this.where), get player () this.players[this.where], get statusBar () document.getElementById('status-bar'), get statusBarVisible () !this.statusBar.getAttribute('moz-collapsed', false), set statusBarVisible (value) (this.statusBar.setAttribute('moz-collapsed', !value), value), get where () { for (let [name, player] in Iterator(this.players)) if (player.isValid) return name; }, addUserCommands: function () { let self = this; function add (cmdName, funcS, funcB) { commands.addUserCommand( ['st' + cmdName], cmdName.replace(/[\[\]]+/g, '') + ' - Stella', (funcS instanceof Function) ? funcS : function (arg, bang) { if (!self.isValid) raise('Stella: Current page is not supported'); let p = self.player; let func = bang ? funcB : funcS; if (p.has(func, 'rwt')) p.toggle(func); else if (p.has(func, 'rw')) p[func] = arg[0]; else if (p.has(func, 'x')) p[func].apply(p, arg); self.update(); }, {argCount: '*', bang: !!funcB}, true ); } add('pl[ay]', 'playOrPause', 'play'); add('pa[use]', 'pause'); add('mu[te]', 'muted'); add('re[peat]', 'repeating'); add('co[mment]', 'comment'); add('vo[lume]', 'volume', 'turnUpDownVolume'); add('se[ek]', 'seek', 'seekRelative'); add('fe[tch]', 'fetch'); add('la[rge]', 'large'); add('fu[llscreen]', 'fullscreen'); commands.addUserCommand( ['strel[ations]'], 'relations - Stella', function (args) { let arg = args.string; let url = (function () { if (self.player.has('makeURL', 'x')) { if (arg.match(/^[#\uff03]/)) return self.player.makeURL(arg.slice(1), Player.REL_ID); if (arg.match(/^[:\uff1a]/)) return self.player.makeURL(arg.slice(1), Player.REL_TAG); if (arg.indexOf('http://') == -1) return self.player.makeURL(encodeURIComponent(arg), Player.REL_TAG); } return arg; })(); liberator.open(url, args.bang ? liberator.NEW_TAB : liberator.CURRENT_TAB); }, { argCount: '*', bang: true, completer: function (context, args) { if (!self.isValid) raise('Stella: Current page is not supported'); if (!self.player.has('relations', 'r')) return; context.title = ['Tag/ID', 'Description']; context.completions = self.player.relations.map(function (rel) { switch (rel.type) { case Player.REL_ID: return ['#' + rel.value.id, rel.value.title]; case Player.REL_TAG: return [':' + rel.value, 'Tag']; case Player.REL_URL: return [rel.value.url, rel.value.title]; } }); }, }, true ); }, createStatusPanel: function () { let self = this; // FIXME function setEvents (name, elem) { ['click', 'popupshowing'].forEach(function (eventName) { let onEvent = self['on' + capitalize(name) + capitalize(eventName)]; onEvent && elem.addEventListener(eventName, function (event) { if (eventName != 'click' || event.button == 0) { onEvent.apply(self, arguments); self.update(); } }, false); }); } function createLabel (store, name, l, r) { let label = store[name] = document.createElement('label'); label.setAttribute('value', '-'); label.style.marginLeft = (l || 0) + 'px'; label.style.marginRight = (r || 0) + 'px'; label.__defineGetter__('text', function () this.getAttribute('value')); label.__defineSetter__('text', function (v) this.setAttribute('value', v)); setEvents(name, label); } let panel = this.panel = document.createElement('statusbarpanel'); panel.setAttribute('id', Stella.MAIN_PANEL_ID); let hbox = document.createElement('hbox'); hbox.setAttribute('align', 'center'); let icon = this.icon = document.createElement('image'); icon.setAttribute('class', 'statusbarpanel-iconic'); icon.style.marginRight = '4px'; setEvents('icon', icon); icon.addEventListener('dblclick', bindr(this, this.onIconDblClick), false); let labels = this.labels = {}; let toggles = this.toggles = {}; createLabel(labels, 'main', 2, 2); createLabel(labels, 'volume', 0, 2); for each (let player in this.players) { for (let func in player.functions) { if (player.has(func, 't')) (func in labels) || createLabel(toggles, func); } } panel.appendChild(hbox); hbox.appendChild(icon); [hbox.appendChild(label) for each (label in labels)]; [hbox.appendChild(toggle) for each (toggle in toggles)]; let menu = this.mainMenu = buildContextMenu({ id: Stella.MAIN_MENU_ID, parent: panel, set: hbox, tree: ContextMenuTree, onAppend: function (elem, menu) setEvents(capitalize(menu.name), elem) }); let stbar = document.getElementById('status-bar'); stbar.insertBefore(panel, document.getElementById('liberator-statusline').nextSibling); let relmenu = document.getElementById('anekos-stela-relations-menupopup'); }, disable: function () { this.hidden = true; if (this.timerHandle) { clearInterval(this.timerHandle); this.timerHandle = null; } }, enable: function () { this.hidden = false; this.icon.setAttribute('src', this.player.icon); for (let name in this.toggles) { this.toggles[name].hidden = !this.player.has(name, 't'); } if (!this.timerHandle) { this.timerHandle = setInterval(bindr(this, this.update), 500); } }, removeStatusPanel: function () { let e = this.panel || document.getElementById(this.panelId); if (e && e.parentNode) e.parentNode.removeChild(e); }, update: function () { this.labels.main.text = this.player.statusText; this.labels.volume.text = this.player.volume; for (let name in this.toggles) { this.toggles[name].text = (this.player[name] ? String.toUpperCase : id)(name[0]); } }, onCommentClick: function () (this.player.toggle('comment')), onFetchClick: function () this.player.fetch(), // フルスクリーン時にステータスバーを隠さないようにする onFullScreen: function () { if (window.fullScreen) { this.__statusBarVisible = this.statusBarVisible; this.statusBarVisible = true; } else { if (this.__statusBarVisible !== undefined) this.statusBarVisible = this.__statusBarVisible; } }, onFullscreenClick: function () this.player.toggle('fullscreen'), onIconClick: function () this.player.playOrPause(), onIconDblClick: function () this.player.toggle('fullscreen'), onLocationChange: function () { if (this.__valid !== this.isValid) { (this.__valid = this.isValid) ? this.enable() : this.disable(); } }, onMainClick: function (event) { if (event.button) return; if (!(this.player && this.player.has('currentTime', 'rw', 'totalTime', 'r'))) return; let rect = event.target.getBoundingClientRect(); let x = event.screenX; let per = (x - rect.left) / (rect.right - rect.left); this.player.currentTime = this.player.totalTime * per; }, onMutedClick: function (event) this.player.toggle('muted'), onPauseClick: function () this.player.pause(), onPlayClick: function () this.player.play(), onRepeatClick: function () this.player.toggle('repeating'), onRelationsRootPopupshowing: function () { /* build */ }, onResize: function () { if (this.__fullScreen !== window.fullScreen) { this.__fullScreen = window.fullScreen; this.onFullScreen(this.__fullScreen); } }, onSetVolumeClick: function (event) (this.player.volume = event.target.getAttribute('volume')) }; fixDoubleClick(Stella.prototype, 'onIconClick', 'onIconDblClick'); // }}} /********************************************************************************* * Install {{{ *********************************************************************************/ if (InVimperator) { let estella = liberator.plugins.stella; let install = function () { let stella = liberator.plugins.stella = new Stella(); stella.addUserCommands(); liberator.log('Stella: installed.'); }; // すでにインストール済みの場合は、一度ファイナライズする // (デバッグ時に前のパネルが残ってしまうため) if (estella) { estella.finalize(); install(); } else { window.addEventListener( 'DOMContentLoaded', function () { window.removeEventListener('DOMContentLoaded', arguments.callee, false); install(); }, false ); } } else { /* do something */ } // }}} })(); // vim:sw=2 ts=2 et si fdm=marker: