MechaLynx / yt-url-at-time

// ==UserScript==
// @name        yt-url-at-time
// @namespace   mechalynx/yt-url-at-time
// @license     MIT
// @grant       none
// @description On youtube, use alt+` to set the url to the current timestamp, for easy bookmarking
// @include     https://www.youtube.com/*
// @version     0.2.7
// @copyright   2017, MechaLynx (https://github.com/MechaLynx)
// @updateURL   https://openuserjs.org/meta/MechaLynx/yt-url-at-time.meta.js
// @downloadURL https://openuserjs.org/src/scripts/MechaLynx/yt-url-at-time.user.js
// @run-at document-idle
// @author      MechaLynx
// ==/UserScript==
// jshint esversion: 6

// Matches time hashes for the purpose of removing them
// note that I don't like my regexp here...
var re_timehash = /#t=(?:[0-9]*\.?[0-9]*|(?:[0-9]*(?:h|m|s))*)*/g;

// `video` element utility
var video = {
  get element() {
    return document.querySelector('#movie_player video');
  },
  get timehash() {
    var secs = this.element.currentTime;
    return '#t=' + [(h = ~~(secs / 3600)) && h + 'h' || null,
    (m = ~~(secs % 3600 / 60)) && m + 'm' || null,
    (s = ~~(secs % 3600 % 60)) && s + 's'].join('');
  },
  get plaintimehash() {
    return '#t=' + this.element.currentTime;
  },
  get notimehash() {
    return window.location.origin +
    window.location.pathname +
    window.location.search +
    window.location.hash.replace(re_timehash, '');
  }
};

// Keep looking for the time indicator span, until it's found
// The `load` event is insufficient
var wait_for_page = window.setInterval(function(){
  var current_time_element = document.querySelector('.ytp-time-current');
  if (current_time_element){
    window.clearInterval(wait_for_page);

    // Add CSS for time indicator span
    let time_style = document.createElement('style');
    time_style.setAttribute('name', "yt-url-at-time");
    time_style.innerHTML = `
      .url-at-time-element-hover:hover{
        cursor: pointer;
      }
      .url-at-time-clipboard-helper{
        position: absolute;
        top: 0;
        left: 0;
        padding: none;
        margin: none;
        border: none;
        width: 0;
        height: 0;
      }
	  `;
    document.body.appendChild(time_style);

    // Toggle the class so that it doesn't look clickable
    // during ads, which would be confusing
    current_time_element.onmouseover = function(){
      if (document.querySelector('.videoAdUi')){
        current_time_element.classList.remove('url-at-time-element-hover');
      }else{
        current_time_element.classList.add('url-at-time-element-hover');
      }
    };

    current_time_element.addEventListener('click', function(e){
      if (e.altKey){
        hashmodifier(true);
      } else {
        hashmodifier(false);
      }

      if (e.ctrlKey){
        copy_url_to_clipboard();
      }
    });
  }
}, 1000);

// Add the timestamp to the URL
var hashmodifier = function(precise=false){
  if ( location.href.match(/.*watch.*/) && document.querySelector('.videoAdUi') === null){
    precise ? history.replaceState(false, false, video.notimehash + video.plaintimehash) : history.replaceState(false, false, video.notimehash + video.timehash);
  }
};

var copy_url_to_clipboard = function(attempt_to_restore=false){
  // Current focus and selection cannot be restored
  // since clicking on the timer causes the movie player to be focused
  // clearing the selection and changing the active element before we arrive here
  // However, attempting to restore them is meaningful if called through a hotkey
  if (attempt_to_restore){
    var selection = document.getSelection();
    var current_selection = selection.getRangeAt(0);
    var current_focus = document.activeElement;
  }

  // Add invisible textarea to allow copying the generated URL to clipboard
  let clipboard_helper = document.createElement('textarea');
  clipboard_helper.classList.add('url-at-time-clipboard-helper');
  document.body.appendChild(clipboard_helper);

  clipboard_helper.value = window.location.href;
  clipboard_helper.select();
  clipboard_helper.setSelectionRange(0, clipboard_helper.value.length);
  document.execCommand('copy');

  document.body.removeChild(clipboard_helper);

  if (attempt_to_restore){
    current_focus.focus();

    // https://gist.github.com/dantaex/543e721be845c18d2f92652c0ebe06aa
    selection.empty();
    selection.addRange(current_selection);
  }
};

var _alt=false;
var _q=false;
// Listen for the hotkey
document.addEventListener('keydown', z => {
  // if you want to change the hotkey
  // you can use this: http://mechalynx.github.io/keypress/
  // or another tester if you don't like this one
  if (z.code === 'KeyQ'){
    _q=true;
  }
  if (z.altKey && z.code === 'Backquote'){
    hashmodifier(_alt);
    _alt=true;
  }
  if (_q && _alt){
    copy_url_to_clipboard(true);
  }
});

document.addEventListener('keyup', z => {
  if(!z.altKey){
    _alt=false;
  }
  if(z.code === "KeyQ"){
    _q=false;
  }
});