diff options
Diffstat (limited to 'stella.js')
-rw-r--r-- | stella.js | 564 |
1 files changed, 564 insertions, 0 deletions
diff --git a/stella.js b/stella.js new file mode 100644 index 0000000..2246071 --- /dev/null +++ b/stella.js @@ -0,0 +1,564 @@ +// ==VimperatorPlugin== +// @name すてら +// @description-ja ステータスラインに動画の再生時間などを表示する。 +// @license Creative Commons Attribution-Share Alike 3.0 Unported +// @version 0.01 +// @author anekos (anekos@snca.net) +// @minVersion 2.0pre +// @maxVersion 2.0pre +// ==/VimperatorPlugin== +// +// Usage-ja: +// 作成中 +// +// TODO +// user command +// :fetchvideo +// Icons +// Other video hosting websites +// +// Links: +// +// License: +// http://creativecommons.org/licenses/by-sa/3.0/ + + +(function () { + + /********************************************************************************* + * Const {{{ + *********************************************************************************/ + + const ID_PREFIX = 'ank-nico-status-'; + + + /********************************************************************************* + * Utils {{{ + *********************************************************************************/ + + function isNum (v) + (typeof v === 'number' && !isNaN(v)); + + function lz (s,n) + String(Math.pow(10,n ) + s).substring(1); + + function toTimeCode(v) + (isNum(v) ? (parseInt((v / 60)) + ':' + lz(v % 60, 2)) + : '??:??'); + + function bindr (_this, f) + function () f.apply(_this, arguments); + + function capitalize (s) + s.replace(/^./, String.toUpperCase); + + + // }}} + + /********************************************************************************* + * Player {{{ + *********************************************************************************/ + + function Player () { + this.initialize.apply(this, arguments); + } + + Player.ST_PLAYING = 'playing'; + Player.ST_PAUSED = 'paused'; + Player.ST_ENDED = 'ended'; + Player.ST_OTHER = 'other'; + + // rwx で機能の有無を表す + Player.prototype = { + functions: { + currentTime: '', + totalTime: '', + volume: '', + play: '', + pause: '', + muted: '', + repeating: '', + }, + + initialize: function () void null, + + icon: null, + + get currentTime () undefined, + set currentTime (value) void value, + + get timeCodes () (toTimeCode(this.currentTime) + '/' + toTimeCode(this.totalTime)), + + get volume () undefined, + set volume (value) void value, + + get statusText () this.timeCodes, + + is: function (state) (this.state == state), + + has: function (name, ms) !Array.some(ms, function (m) this.functions[name].indexOf(m) < 0), + + playOrPause: function () { + if (this.is(Player.ST_PLAYING)) { + this.pause(); + } else { + this.playEx(); + } + }, + + play: function () undefined, + + playEx: function () { + if (this.is(Player.ST_ENDED)) + this.currentTime = 0; + this.play(); + }, + + pause: function () undefined, + + get repeating () undefined, + set repeating (value) undefined, + + get muted () undefined, + set muted (value) undefined, + + get state () undefined, + + toggle: function (name) { + if (!this.has(name, 'rw')) + return; + let v = this[name]; + this[name] = !v; + return !v; + } + }; + + + /********************************************************************************* + * YouTubePlayer {{{ + *********************************************************************************/ + + function YouTubePlayer () { + Player.apply(this, arguments); + } + + YouTubePlayer.prototype = { + __proto__: Player.prototype, + + functions: { + currentTime: 'rw', + totalTime: 'r', + volume: 'rw', + play: 'x', + pause: 'x', + muted: 'rw', + repeating: 'rw', + }, + + icon: 'http://www.youtube.com/favicon.ico', + + get player () + let (p = content.document.getElementById('movie_player')) + (p && (p.wrappedJSObject || p)), + + get currentTime () parseInt(this.player.getCurrentTime()), + set currentTime (value) this.player.seekTo(value), + + get totalTime () parseInt(this.player.getDuration()), + + get volume () parseInt(this.player.getVolume()), + set volume (value) parseInt(this.player.setVolume(value)), + + play: function () this.player.playVideo(), + + pause: function () this.player.pauseVideo(), + + get muted () this.player.isMuted(), + set muted (value) (value ? this.player.mute() : this.player.unMute()), + + get state () { + switch (this.player.getPlayerState()) { + case 'ended': + return Player.ST_ENDED; + case 'playing': + return Player.ST_PLAYING; + case 'paused': + return Player.ST_PAUSED; + case 'buffering': + case 'video cued': + case 'unstarted': + default: + return Player.ST_OTHER; + } + } + }; + + // }}} + + /********************************************************************************* + * NicoPlayer {{{ + *********************************************************************************/ + + function NicoPlayer () { + Player.apply(this, arguments); + } + + NicoPlayer.prototype = { + __proto__: Player.prototype, + + functions: { + currentTime: 'rw', + totalTime: 'r', + volume: 'rw', + play: 'x', + pause: 'x', + muted: 'rw', + repeating: 'rw', + comment: 'rw' + }, + + icon: 'http://www.nicovideo.jp/favicon.ico', + + get player () + let (p = content.document.getElementById('flvplayer')) + (p && (p.wrappedJSObject || p)), + + get currentTime () parseInt(this.player.ext_getPlayheadTime()), + set currentTime (value) this.player.ext_setPlayheadTime(value), + + get totalTime () parseInt(this.player.ext_getTotalTime()), + + get volume () parseInt(this.player.ext_getVolume()), + set volume (value) parseInt(this.player.ext_setVolume(value)), + + 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); + } + }, + + play: function () this.player.ext_play(true), + + pause: function () this.player.ext_play(false), + + get comment () this.player.ext_isCommentVisible(), + set comment (value) this.player.ext_setCommentVisible(value), + + get repeating () this.player.ext_isRepeat(), + set repeating (value) this.player.ext_setRepeat(value), + + get muted () this.player.ext_isMute(), + set muted (value) this.player.ext_setMute(value), + + 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; + } + } + }; + + // }}} + + /********************************************************************************* + * 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', + { + name: 'volume-root', + label: 'Volume', + id: ID_PREFIX + 'volume-menupopup', + sub: ContextMenuVolume + } + ]; + + 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; + } + + // }}} + + /********************************************************************************* + * NicoStatusLine {{{ + *********************************************************************************/ + + function NicoStatusLine () { + this.initialize.apply(this, arguments); + } + + NicoStatusLine.MAIN_PANEL_ID = ID_PREFIX + 'panel', + NicoStatusLine.MAIN_MENU_ID = ID_PREFIX + 'main-menu', + NicoStatusLine.VOLUME_MENU_ID = ID_PREFIX + 'volume-menu', + + NicoStatusLine.prototype = { + // new 時に呼ばれる + initialize: function () { + this.players = { + niconico: new NicoPlayer(), + youtube: new YouTubePlayer() + }; + this.createStatusPanel(); + this.addAutoCommand(); + this.onLocationChange(); + this.__onResize = window.addEventListener('resize', bindr(this, this.onResize), false); + }, + + // もちろん、勝手に呼ばれたりはしない。 + finalize: function () { + this.removeStatusPanel(); + this.disable(); + window.removeEventListener('resize', this.__onResize, false); + }, + + get where () ( + (~buffer.URL.indexOf('http://www.nicovideo.jp/watch/') && 'niconico') + || + (buffer.URL.match(/^http:\/\/(?:[^.]+\.)?youtube\.com\/watch/) && 'youtube') + ), + + get hidden () (this.panel.hidden), + set hidden (v) (this.panel.hidden = v), + + get valid () (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), + + setLabelText: function (name, text) + let (label = this.labels[name]) + (label && label.setAttribute('value', text)), + + removeStatusPanel: function () { + let e = this.panel || document.getElementById(this.panelId); + if (e && e.parentNode) + e.parentNode.removeChild(e); + }, + + createStatusPanel: function () { + let self = this; + + function setClickEvent (name, elem) { + let onClick = self['on' + capitalize(name) + 'Click']; + onClick && elem.addEventListener('click', function (event) { + if (event.button == 0) { + onClick.apply(self, arguments); + self.update(); + } + }, false); + } + + let panel = this.panel = document.createElement('statusbarpanel'); + panel.setAttribute('id', this.panelId); + + 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'; + setClickEvent('icon', icon); + + let labels = this.labels = {}; + ['main', 'volume', 'comment', 'repeat'].forEach(function (name, index) { + let label = labels[name] = document.createElement('label'); + label.setAttribute('value', '-'); + label.style.marginLeft = (index < 2 ? 2 : 0) + 'px'; + (index < 3) && (label.style.marginRight = '0px'); + setClickEvent(name, label); + }); + + panel.appendChild(hbox); + hbox.appendChild(icon); + [hbox.appendChild(label) for each (label in labels)]; + + let menu = this.mainMenu = buildContextMenu({ + id: NicoStatusLine.MAIN_MENU_ID, + parent: panel, + set: hbox, + tree: ContextMenuTree, + onAppend: function (elem, menu) setClickEvent(capitalize(menu.name), elem) + }); + + let volMenu = this.volumeMenu = buildContextMenu({ + id: NicoStatusLine.VOLUME_MENU_ID, + parent: panel, + set: labels.volume, + tree: ContextMenuVolume, + onAppend: function (elem, menu) setClickEvent(capitalize(menu.name), elem) + }); + + labels.volume.setAttribute('context', volMenu.id); + + liberator.log(menu.id) + + let stbar = document.getElementById('status-bar'); + stbar.insertBefore(panel, document.getElementById('liberator-statusline').nextSibling); + }, + + // FIXME + addAutoCommand: function () + autocommands.add('LocationChange', /.*/, "js liberator.plugins.nico_statusline.onLocationChange()"), + + update: function () { + try { + this.setLabelText('main', this.player.statusText); + this.setLabelText('comment', this.player.comment ? 'C' : 'c'); + this.setLabelText('repeat', this.player.repeating ? 'R' : 'r'); + this.setLabelText('volume', this.player.muted ? 'M' : this.player.volume); + } catch (e) { + liberator.log(e); + } + }, + + enable: function () { + this.hidden = false; + this.icon.setAttribute('src', this.player.icon); + ['comment', 'repeat', 'volume'].forEach(bindr(this, function (name) { + this.labels[name].hidden = !this.player.functions[name]; + })); + if (!this.timerHandle) { + this.timerHandle = setInterval(bindr(this, this.update), 500); + } + }, + + disable: function () { + this.hidden = true; + if (this.timerHandle) { + clearInterval(this.timerHandle); + this.timerHandle = null; + } + }, + + onLocationChange: function () { + if (this.__valid !== this.valid) { + (this.__valid = this.valid) ? this.enable() : this.disable(); + } + }, + + onPlayClick: function () this.player.play(), + + onPauseClick: function () this.player.pause(), + + onVolumeClick: function (event) (this.player.muted = !this.player.muted), + + onSetVolumeClick: function (event) (this.player.volume = event.target.getAttribute('volume')), + + onCommentClick: function () (this.player.comment = !this.player.comment), + + onRepeatClick: function () (this.player.repeating = !this.player.repeating), + + onMainClick: function (event) { + if (event.button) + 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; + }, + + onIconClick: function () this.player.playOrPause(), + + // フルスクリーン時にステータスバーを隠さないようにする + onFullScreen: function () { + if (window.fullScreen) { + this.__statusBarVisible = this.statusBarVisible; + this.statusBarVisible = true; + } else { + if (this.__statusBarVisible !== undefined) + this.statusBarVisible = this.__statusBarVisible; + } + }, + + onResize: function () { + if (this.__fullScreen !== window.fullScreen) { + this.__fullScreen = window.fullScreen; + this.onFullScreen(this.__fullScreen); + } + } + }; + + // }}} + + /********************************************************************************* + * Install {{{ + *********************************************************************************/ + + let (nsl = liberator.plugins.nico_statusline) { + if (nsl) { + nsl.finalize(); + liberator.plugins.nico_statusline = new NicoStatusLine(); + } else { + window.addEventListener( + 'DOMContentLoaded', + function () { + window.removeEventListener('DOMContentLoaded', arguments.callee, false); + liberator.plugins.nico_statusline = new NicoStatusLine(); + }, + false + ); + } + } + + // }}} + +})(); + +// vim:sw=2 ts=2 et si fdm=marker: |