');
            let r = re.exec(comment);
            if (r)
              videos.push(new RelatedID(link, r[1].slice(-20)));
          });
        } catch (e) {
          U.log('stella: ' + e)
        }
        return videos;
      }
      function tagsFromPage () {
        let nodes = content.document.getElementsByClassName('nicopedia');
        return [new RelatedTag(it.textContent) for each (it in nodes) if (it.rel == 'tag')];
      }
      return [].concat(IDsFromComment(), IDsFromAPI(), tagsFromPage());
    },
    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) {
        if (value && !this.large) {
          let win = Buffer.findScrollableWindow();
          this.storage.scrollPositionBeforeLarge = {x: win.scrollX, y: win.scrollY};
        }
        this.player.ext_setVideoSize(value ? NicoPlayer.SIZE_LARGE : NicoPlayer.SIZE_NORMAL);
        let pos = this.storage.scrollPositionBeforeLarge;
        if (!value && typeof pos != "undefined")
            setTimeout(function () buffer.scrollTo(pos.x, pos.y), 0);
        this.last.screenMode = this.large ? 'large' : null;
        return this.large;
    },
    get state () {
      switch (this.player.ext_getStatus()) {
        case 'end':
          return Player.ST_ENDED;
        case 'playing':
          return this.storage.bug_paused ? Player.ST_PAUSED : 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 self = this;
      let watchURL = U.currentURL;
      let [,id] = watchURL.match(/watch\/(.+)$/);
      let apiURL = 'http://www.nicovideo.jp/api/getflv?v=' + id;
      U.httpRequest(
        watchURL,
        null,
        function () {
          U.httpRequest(
            'http://www.nicovideo.jp/api/getflv?v=' + self.id,
            null,
            function (xhr) {
              let res = xhr.responseText;
              let info = {};
              res.split(/&/).forEach(function (it) let ([n, v] = it.split(/=/)) (info[n] = v));
              U.download(decodeURIComponent(info.url), filepath, self.fileExtension, self.title);
              let postData = '';
              // FIXME
              let msgFilepath = filepath.replace(/\.[^\.]+$/, '.xml');
              U.download(decodeURIComponent(info.ms), msgFilepath, '.xml', self.title, postData);
            }
          );
        }
      );
    },
    makeURL: function (value, type) {
      switch (type) {
        case Player.URL_ID:
          return 'http://www.nicovideo.jp/watch/' + value;
        case Player.URL_TAG:
          return 'http://www.nicovideo.jp/tag/' + encodeURIComponent(value);
        case Player.URL_SEARCH:
          return 'http://www.nicovideo.jp/search/' + encodeURIComponent(value);
      }
      return value;
    },
    pause: function () {
      this.storage.bug_paused = true;
      this.player.ext_play(false);
    },
    play: function () {
      this.storage.bug_paused = false;
      this.player.ext_play(true)
    },
    playOrPause: function () {
      if (this.is(Player.ST_PLAYING)) {
        this.pause();
      } else {
        let base = this.currentTime;
        setTimeout(U.bindr(this, function () (base === this.currentTime ? this.playEx() : this.pause())), 100);
      }
    },
    say: function (message) {
      U.log('stsay');
      this.sendComment(message);
    },
    // みかんせいじん
    // test -> http://www.nicovideo.jp/watch/sm2586636
    // 自分のコメントが見れないので、うれしくないかも。
    sendComment: function (message, command, vpos) {
      let self = this;
      // コメント連打を防止
      {
        let now = new Date();
        let last = this.__last_comment_time;
        if (last && (now.getTime() - last.getTime()) < 5000)
          return U.raise('Shurrup!!');
        this.__last_comment_time = now;
      }
      function getThumbInfo () {
        U.log('getThumbInfo');
        if (self.cachedInfo.block_no !== undefined)
          return;
        let xhr = U.httpRequest(self.baseURL + 'api/getthumbinfo/' + self.id);
        let xml = xhr.responseXML;
        let cn = xml.getElementsByTagName('comment_num')[0];
        self.cachedInfo.block_no = cn.textContent.replace(/..$/, '');
      }
      function getFLV () {
        U.log('getFLV');
        if (self.cachedInfo.flvInfo !== undefined)
          return;
        let xhr = U.httpRequest(self.baseURL + 'api/getflv?v=' + self.id);
        let res = xhr.responseText;
        self.cachedInfo.flvInfo = U.parseParameter(res);
      }
      function getPostkey () {
        U.log('getPostkey');
        let info = self.cachedInfo;
        if (info.postkey !== undefined)
          return;
        let url = U.fromTemplate(
                    '--base--api/getpostkey?thread=--thread_id--&block_no=--block_no--',
                    {
                      base: self.baseURL,
                      thread_id: info.flvInfo.thread_id,
                      block_no: info.block_no
                    }
                  );
        U.log(url);
        let xhr = U.httpRequest(url);
        let res = xhr.responseText;
        info.postkey = res.replace(/^.*=/, '');
      }
      function getComments () {
        U.log('getComments');
        let info = self.cachedInfo;
        if (info.ticket !== undefined)
          return;
        let tmpl = '';
        let xhr = U.httpRequest(info.flvInfo.ms, U.fromTemplate(tmpl, info.flvInfo));
        let xml = xhr.responseXML;
        let r = xml.evaluate('//packet/thread', xml, null, 9, null, 7, null).singleNodeValue;
        info.ticket = r.getAttribute('ticket');
      }
      function sendChat () {
        U.log('sendChat');
        let info = self.cachedInfo;
        let tmpl = '--body--';
        let args = {
          __proto__: info.flvInfo,
          ticket: info.ticket,
          postkey: info.postkey,
          vpos: vpos * 100,
          mail: command,
          body: message
        };
        U.log(args);
        let data = U.fromTemplate(tmpl, args);
        let xhr = U.httpRequest(info.flvInfo.ms, data);
        U.log(xhr.responseText);
      }
      function sendDummyComment (message, command, position) {
        self.player.ext_sendLocalMessage(message, command, vpos);
      }
      // 0 秒コメントはうざいらしいので勝手に自重する
      vpos = Math.max(1, parseInt(vpos || self.currentTime, 10));
      U.log('sendcommnet');
      getThumbInfo();
      getFLV();
      getPostkey();
      getComments();
      sendChat();
      sendDummyComment(message, command, vpos);
    }
  };
  // }}}
  /*********************************************************************************
  * VimeoPlayer                                                                  {{{
  *********************************************************************************/
  function VimeoPlayer () {
    Player.apply(this, arguments);
  }
  VimeoPlayer.getIDfromURL = function (url) let ([_, r] = url.match(/[?;&]v=([-\w]+)/)) r;
  VimeoPlayer.prototype = {
    __proto__: Player.prototype,
    functions: {
      currentTime: 'w',
      fetch: 'x',
      makeURL: 'x',
      muted: 'w',
      pause: 'x',
      play: 'x',
      playEx: 'x',
      playOrPause: 'x',
      title: 'r'
    },
    __initializePlayer: function (player) {
      if (!player || player.__stella_initialized)
        return player;
      player.__stella_mute = false;
      player.__stella_volume = 100;
      player.__stella_initialized = true;
      return player;
    },
    icon: 'http://www.vimeo.com/favicon.ico',
    set currentTime (value) (this.player.api_seekTo(U.fromTimeCode(value)), value),
    get muted () this.__mute,
    set muted (value) (this.volume = value ? 0 : 100),
    get player ()
      this.__initializePlayer(content.document.querySelector('.vimeo_holder * object').wrappedJSObject),
    get ready () !!this.player,
    get state () {
      if (this.player.api_isPlaying())
        return Player.ST_PLAYING
      if (this.player.api_isPaused())
        return Player.ST_PAUSED;
      return Player.ST_OTHER;
    },
    get title ()
      U.xpathGet('//div[@class="title"]').textContent,
    get isValid () U.currentURL.match(/^http:\/\/(www\.)?vimeo\.com\/(channels\/(hd)?#)?\d+$/),
    // XXX setVolume は実際には存在しない?
    get volume () parseInt(this.player.__stella_volume),
    set volume (value) (this.api_setVolume(value), this.player.__stella_volume = value),
    fetch: function(filepath) {
      let self = this;
      let id = U.currentURL.match(/vimeo\.com\/(\d+)/)[1];
      U.httpRequest(
        'http://www.vimeo.com/moogaloop/load/clip:' + id,
        null,
        function(xhr) {
          let doc = xhr.responseXML;
          let signature = U.xpathGet('/xml/request_signature', doc).textContent;
          let timestamp = U.xpathGet('/xml/timestamp', doc).textContent;
          let isHD = parseInt(U.xpathGet('/xml/video/isHD', doc).textContent);
          let url = 'http://www.vimeo.com/moogaloop/play/clip:' + id
            + '/' + signature + '/' + timestamp
            + '/?q=' + (isHD ? 'hd' : 'sd');
          U.download(url, filepath, isHD ? '.mp4' : '.flv', self.title);
        }
      );
    },
    makeURL: function (value, type) {
      switch (type) {
        case Player.URL_ID:
          return 'http://www.vimeo.com/' + value;
        case Player.URL_SEARCH:
          return 'http://www.vimeo.com/videos/search:' + encodeURIComponent(value);
      }
      return value;
    },
    play: function () this.player.api_play(),
    pause: function () this.player.api_pause()
  };
  // }}}
  /*********************************************************************************
  * 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',
    'repeating',
    '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: []
    },
    'cancel',
  ];
  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 = U.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', U.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 (setting) {
    this.initialize.apply(this, arguments);
    this.setting = setting;
  }
  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(this.stella),
        youtube: new YouTubePlayer(this.stella),
        youtubeuc: new YouTubeUserChannelPlayer(this.stella),
        vimeo: new VimeoPlayer(this.stella)
      };
      // this.noGUI = true;
      this.createGUI();
      this.__onResize = window.addEventListener('resize', U.bindr(this, this.onResize), false);
      this.progressListener = new WebProgressListener({onLocationChange: U.bindr(this, this.onLocationChange)});
    },
    createGUI: function () {
      if (this.noGUI)
        return;
      this.createStatusPanel();
      this.onLocationChange();
      this.hidden = true;
    },
    // もちろん、勝手に呼ばれたりはしない。
    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.where && 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 storage ()
      (content.document.__stella_storage || (content.document.__stella_storage = {})),
    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) {
                if (!self.isValid)
                  U.raiseNotSupportedPage();
                let p = self.player;
                let func = arg.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);
                else
                  U.raiseNotSupportedFunction();
                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');
      if (U.s2b(liberator.globalVariables.stella_nico_use_comment, false))
        add('sa[y]', 'say');
      commands.addUserCommand(
        ['stfe[tch]'],
        'Download movie file - Stella',
        function (args) {
          if (!self.isValid)
            return U.raiseNotSupportedPage();
          if (!self.player.has('fetch', 'x'))
            return U.raiseNotSupportedFunction();
          self.player.fetch(args.literalArg);
        },
        {
          literal: 0,
          completer: function (context) completion.file(context)
        },
        true
      );
      commands.addUserCommand(
        ['stqu[ality]'],
        'Quality - Stella',
        function (args) {
          if (!self.isValid)
            return U.raiseNotSupportedPage();
          if (!self.player.has('quality', 'w'))
            return U.raiseNotSupportedFunction();
          self.player.quality = args.literalArg;
        },
        {
          literal: 0,
          completer: function (context) {
            if (!self.player.has('qualities', 'r'))
              return;
            context.title = ['Quality', 'Description'];
            context.completions = [[q, q] for each ([, q] in self.player.qualities)];
          }
        },
        true
      );
      let (lastCompletions = []) {
        commands.addUserCommand(
          ['strel[ations]'],
          'relations - Stella',
          function (args) {
            if (!self.isValid)
              return U.raiseNotSupportedPage();
            let arg = args.literalArg;
            let index = /^\d+:/.test(arg) && parseInt(arg, 10);
            if (index > 0)
              arg = lastCompletions[index - 1].command;
            let url = self.player.has('makeURL', 'x') ? makeRelationURL(self.player, arg) : arg;
            liberator.open(url, args.bang ? liberator.NEW_TAB : liberator.CURRENT_TAB);
          },
          {
            literal: 0,
            argCount: '*',
            bang: true,
            completer: function (context, args) {
              if (!self.isValid)
                U.raiseNotSupportedPage();
              if (!self.player.has('relations', 'r'))
                U.raiseNotSupportedFunction();
              context.filters = [CompletionContext.Filter.textDescription];
              context.anchored = false;
              context.title = ['Tag/ID', 'Description'];
              context.keys = {text: 'text', description: 'description', thumbnail: 'thumbnail'};
              let process = Array.slice(context.process);
              context.process = [
                process[0],
                function (item, text)
                  (item.thumbnail ? <>
{text}>
                                  : process[1].apply(this, arguments))
              ];
              lastCompletions = self.player.relations;
              context.completions = lastCompletions.map(function (rel) rel.completionItem);
            },
          },
          true
        );
      }
    },
    addPageInfo: function () {
      let self = this;
      delete buffer.pageInfo.S;
      buffer.addPageInfoSection(
        'S',
        'Stella Info',
        function (verbose)
          (self.isValid && self.player.has('pageinfo', 'r')
            ? [
                [n, {modules.template.maybeXML(v)}
]
                for each ([n, v] in self.player.pageinfo)
              ]
            : [])
      );
    },
    createStatusPanel: function () {
      let self = this;
      function setEvents (name, elem) {
        ['click', 'command', 'popupshowing'].forEach(function (eventName) {
          let onEvent = self[
            'on' +
              U.capitalize(name) +
              U.capitalize(eventName == 'command' ? 'click' : eventName)
          ];
          onEvent && elem.addEventListener(eventName, function (event) {
            if (eventName == 'click' && event.button != 0)
              return;
            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', U.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(U.capitalize(menu.name), elem)
      });
      let stbar = document.getElementById('status-bar');
      stbar.appendChild(panel);
      let relmenu = document.getElementById('anekos-stella-relations-menupopup');
      panel.addEventListener('DOMMouseScroll', U.bindr(this, this.onMouseScroll), true);
    },
    disable: function () {
      if (this.noGUI)
        return;
      this.hidden = true;
      if (this.__updateTimer) {
        clearInterval(this.__updateTimer);
        delete this.__updateTimer;
      }
      if (this.__autoFullscreenTimer) {
        clearInterval(this.__autoFullscreenTimer);
      }
    },
    enable: function () {
      if (this.noGUI)
        return;
      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.__updateTimer) {
        this.__updateTimer = setInterval(U.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 () {
      if (!(this.isValid && this.player.ready))
        return;
      this.labels.main.text =
        let (v = this.player.statusText)
          (this.__currentTimeTo == undefined) ? v
                                              : v.replace(/^\d*\:\d*/,
                                                          U.toTimeCode(this.__currentTimeTo));
      this.labels.volume.text = this.player.volume;
      for (let name in this.toggles) {
        this.toggles[name].text = (this.player[name] ? String.toUpperCase : U.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'),
    onLargeClick: function () {
      // XXX fullscreen と large の実装が同じ場合に問題になるので、toggle は使わない
      let old = this.player.large;
      if (this.player.fullscreen)
        this.player.fullscreen = false;
      this.player.large = !old;
    },
    onLocationChange: function () {
      if (this.__valid !== this.isValid) {
        (this.__valid = this.isValid) ? this.enable() : this.disable();
      }
      if (this.isValid) {
        clearInterval(this.__onReadyTimer);
        this.__onReadyTimer = setInterval(
          U.bindr(this, function () {
            if (this.player && this.player.ready) {
              clearInterval(this.__onReadyTimer);
              delete this.__onReadyTimer;
              this.onReady();
            }
          }),
          200
        );
      }
    },
    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 = parseInt(this.player.totalTime * per);
    },
    onMouseScroll: (function () {
      let timerHandle;
      return function (event) {
        if (!(this.isValid && this.player.ready && event.detail))
          return;
        if (event.target == this.labels.main) {
          if (this.__currentTimeTo == undefined)
            this.__currentTimeTo = this.player.currentTime;
          this.__currentTimeTo += (event.detail > 0) ? -5 : 5;
          this.__currentTimeTo = Math.min(Math.max(this.__currentTimeTo, 0), this.player.totalTime);
          clearTimeout(timerHandle);
          timerHandle = setTimeout(
            U.bindr(this, function () {
              this.player.currentTime = this.__currentTimeTo;
              delete this.__currentTimeTo;
            }),
            1000
          );
          this.update();
        } else {
          this.player.volume += (event.detail > 0) ? -5 : 5;
          this.update();
        }
      }
    })(),
    onMutedClick: function (event) this.player.toggle('muted'),
    onPauseClick: function () this.player.pause(),
    onPlayClick: function () this.player.play(),
    onReady: function () {
      if (this.player.last.screenMode && !this.storage.alreadyAutoFullscreen
      && !this.__autoFullscreenTimer) {
        this.__autoFullscreenTimer = setInterval(
          U.bindr(this, function () {
            if (!this.player.ready)
              return;
            clearInterval(this.__autoFullscreenTimer)
            setTimeout(
              U.bindr(this, function () (this.player[this.player.last.screenMode] = true)),
              this.setting.common.autoFullscreenDelay
            );
            delete this.__autoFullscreenTimer;
          }),
          200
        );
      }
      this.storage.alreadyAutoFullscreen = true;
    },
    onRepeatingClick: function () this.player.toggle('repeating'),
    onRelationsRootPopupshowing: function () {
      let self = this;
      function clickEvent (cmd)
        function () liberator.open(makeRelationURL(self.player, cmd));
      if (!this.player)
        return;
      let relmenu = document.getElementById('anekos-stella-relations-menupopup');
      let rels = this.player.relations;
      while (relmenu.firstChild)
        relmenu.removeChild(relmenu.firstChild);
      rels.forEach(function (rel) {
        let elem = document.createElement('menuitem');
        let prefix = rel instanceof RelatedID  ? 'ID: ' :
                     rel instanceof RelatedTag ? 'Tag: ' :
                     '';
        elem.setAttribute('label', prefix + rel.description);
        elem.addEventListener('command', clickEvent(rel.command), false);
        relmenu.appendChild(elem);
      }, this);
    },
    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'))
  };
  U.fixDoubleClick(Stella.prototype, 'onIconClick', 'onIconDblClick');
  // }}}
  /*********************************************************************************
  * Functions                                                                    {{{
  *********************************************************************************/
  function makeRelationURL (player, command) {
    if (!player.has('makeURL', 'x'))
      U.raise('Mysterious Error! makeURL has been not implmented.');
    if (command.match(/^[#\uff03]/))
      return player.makeURL(command.slice(1), Player.URL_ID);
    if (command.match(/^[:\uff1a]/))
      return player.makeURL(command.slice(1), Player.URL_TAG);
    if (command.indexOf('http://') == -1)
      return player.makeURL(encodeURIComponent(command), Player.URL_TAG);
    return command;
  }
  // }}}
  /*********************************************************************************
  * Install                                                                      {{{
  *********************************************************************************/
  if (InVimperator) {
    let estella = liberator.globalVariables.stella;
    let install = function () {
      let stella = liberator.globalVariables.stella = new Stella(new Setting());
      stella.addUserCommands();
      stella.addPageInfo();
      U.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: