Greasy Fork镜像 支持简体中文。

* Lyric FullScreen Columnizer

It offers a full-width and columnized lyric view on major lyric services. No more scrolling while singing, playing the piano, or guitar.

目前為 2021-01-02 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name * Lyric FullScreen Columnizer
  3. // @name:ja * Lyric FullScreen Columnizer
  4. // @name:zh-CN * Lyric FullScreen Columnizer
  5. // @namespace knoa.jp
  6. // @description It offers a full-width and columnized lyric view on major lyric services. No more scrolling while singing, playing the piano, or guitar.
  7. // @description:ja 大手歌詞サイトの歌詞を、横幅いっぱいの複数カラム表示に。歌いながら、ピアノやギターを弾きながら、スクロールしなくてもいいんです。
  8. // @description:zh-CN 将大型歌词网站的歌词显示为宽度最大的多列。一边唱歌,一边弹钢琴和吉他,不用滚动。
  9. // @include https://www.google.*/*Lyric*
  10. // @include https://www.google.*/*%E6%AD%8C%E8%A9%9E*
  11. // @include https://www.google.*/*%E6%AD%8C%E8%AF%8D*
  12. // @include https://www.azlyrics.com/lyrics/*
  13. // @include https://genius.com/*
  14. // @include https://www.lyrics.com/lyric/*
  15. // @include https://j-lyric.net/artist/*
  16. // @include http*://www.kget.jp/lyric/*
  17. // @include https://www.uta-net.com/song/*
  18. // @include https://utaten.com/lyric/*
  19. // @noframes
  20. // @version 2.2.0
  21. // @grant none
  22. // ==/UserScript==
  23.  
  24. (function(){
  25. const SCRIPTID = 'LyricFullScreenColumnizer';
  26. const SCRIPTNAME = '* Lyric FullScreen Columnizer';
  27. const DEBUG = false;/*
  28. [update]
  29. Now available on Genius Lyrics.
  30.  
  31. [possible]
  32. lyrics.com はpreなので単語が切れる。br挿入してnormalテキストにすれば解決するが。
  33. うたまっぷ は大手だがHTMLが古いのでいまのところ対応しない。
  34.  
  35. [acknowledgement]
  36. This script is originally dedicated to Milky Queen, for singing freely with her guitar playing.
  37. 🌾👑 https://twitter.com/milkyqueen_idol
  38. */
  39. if(window === top) console.time(SCRIPTID);
  40. const MS = 1, SECOND = 1000*MS, MINUTE = 60*SECOND, HOUR = 60*MINUTE, DAY = 24*HOUR, WEEK = 7*DAY, MONTH = 30*DAY, YEAR = 365*DAY;
  41. const sites = {
  42. google: {
  43. /* it doesn't detect url with "lyric" or something here, but @include meta tag does */
  44. url: /^https:\/\/www\.google\.[^/]+\//,
  45. targets: {
  46. header: () => $('#sfcnt'),
  47. lyricBody: () => $('[data-lyricid]'),
  48. },
  49. actions: {
  50. beforeColumnize: () => $('g-more-link [aria-expanded="false"]', e => e.click()),
  51. }
  52. },
  53. azlyrics: {
  54. url: /^https:\/\/www\.azlyrics\.com\/lyrics\//,
  55. targets: {
  56. header: () => $('.lboard-wrap'),
  57. lyricBody: () => $('.main-page br + br + div'),
  58. },
  59. },
  60. genius: {
  61. url: /^https:\/\/genius\.com\//,
  62. targets: {
  63. header: () => $('.header'),
  64. lyricBody: () => $('.lyrics'),
  65. },
  66. },
  67. lyrics: {
  68. url: /^https:\/\/www\.lyrics\.com\/lyric\//,
  69. targets: {
  70. header: () => $('#content-top'),
  71. lyricBody: () => $('#lyric-body-text'),
  72. },
  73. },
  74. jlyric: {
  75. url: /^https:\/\/j-lyric\.net\/artist\//,
  76. targets: {
  77. header: () => $('#ttb'),
  78. lyricBody: () => $('#Lyric'),
  79. },
  80. },
  81. kget: {
  82. url: /^https?:\/\/www\.kget\.jp\/lyric\//,
  83. targets: {
  84. header: () => $('#searchbar-wrap'),
  85. lyricBody: () => $('#lyric-trunk'),
  86. },
  87. },
  88. utanet: {
  89. url: /^https:\/\/www\.uta-net\.com\/song\//,
  90. targets: {
  91. header: () => $('#global_header'),
  92. lyricBody: () => $('#kashi_area'),
  93. },
  94. },
  95. utaten: {
  96. url: /^https:\/\/utaten\.com\/lyric\//,
  97. targets: {
  98. header: () => $('body > header'),
  99. lyricBody: () => $('.lyricBody'),
  100. },
  101. },
  102. };
  103. let site;
  104. let elements = {};
  105. const core = {
  106. initialize: function(){
  107. elements.html = document.documentElement;
  108. elements.html.classList.add(SCRIPTID);
  109. site = core.getSite(sites);
  110. if(site){
  111. core.ready();
  112. core.addStyle('style');
  113. core.addStyle('style-' + site.key);
  114. }
  115. },
  116. ready: function(){
  117. core.getTargets(site.targets).then(() => {
  118. log("I'm ready.");
  119. core.bindKeys();
  120. }).catch(e => {
  121. console.error(`${SCRIPTID}:`, e);
  122. });
  123. },
  124. bindKeys: function(){
  125. const {header, lyricBody} = elements;
  126. const beforeLyricBody = elements.lyricBody?.previousElementSibling;
  127. const parentOfLyricBody = elements.lyricBody?.parentNode;
  128. window.addEventListener('keydown', e => {
  129. if(['input', 'textarea'].includes(e.target.localName) || e.target.isContentEditable) return;
  130. if(e.ctrlKey || e.altKey || e.shiftKey || e.metaKey) return;
  131. console.log(SCRIPTID, e.key);
  132. switch(e.key){
  133. /* columnize */
  134. case('1'):
  135. case('2'):
  136. case('3'):
  137. case('4'):
  138. case('5'):
  139. case('6'):
  140. case('7'):
  141. case('8'):
  142. case('9'):
  143. document.body.classList.add(SCRIPTID);
  144. if(site.actions?.beforeColumnize) site.actions.beforeColumnize();
  145. if(document.fullscreenElement === null) header.after(lyricBody);
  146. lyricBody.dataset.columns = e.key;
  147. e.preventDefault();
  148. break;
  149. /* reset to default */
  150. case('0'):
  151. case('Escape'):
  152. document.body.classList.remove(SCRIPTID);
  153. if(beforeLyricBody) beforeLyricBody.after(lyricBody);
  154. else parentOfLyricBody.prepend(lyricBody);
  155. delete lyricBody.dataset.columns;
  156. e.preventDefault();
  157. break;
  158. /* browser's fullscreen */
  159. case('f'):
  160. if(document.fullscreenElement === null){
  161. document.body.classList.add(SCRIPTID);
  162. if(site.actions?.beforeColumnize) site.actions.beforeColumnize();
  163. if(lyricBody.dataset.columns === undefined) lyricBody.dataset.columns = '1';
  164. lyricBody.requestFullscreen();
  165. }
  166. else document.exitFullscreen();
  167. e.preventDefault();
  168. break;
  169. }
  170. }, true);
  171. /* fire the reset event on fullscreen exit */
  172. window.addEventListener('fullscreenchange', e => {
  173. if(document.fullscreenElement) return;
  174. else window.dispatchEvent(new KeyboardEvent('keydown', {key: 'Escape'}));
  175. });
  176. },
  177. getSite: function(sites){
  178. Object.keys(sites).forEach(key => sites[key].key = key);
  179. let key = Object.keys(sites).find(key => sites[key].url.test(location.href));
  180. if(key === undefined) return log('Doesn\'t match any sites:', location.href);
  181. else return sites[key];
  182. },
  183. getTarget: function(selector, retry = 10, interval = 1*SECOND){
  184. const key = selector.name;
  185. const get = function(resolve, reject){
  186. let selected = selector();
  187. if(selected === null || selected.length === 0){
  188. if(--retry) return log(`Not found: ${key}, retrying... (${retry})`), setTimeout(get, interval, resolve, reject);
  189. else return reject(new Error(`Not found: ${selector.name}, I give up.`));
  190. }else{
  191. if(selected.nodeType === Node.ELEMENT_NODE) selected.dataset.selector = key;/* element */
  192. else selected.forEach((s) => s.dataset.selector = key);/* elements */
  193. elements[key] = selected;
  194. resolve(selected);
  195. }
  196. };
  197. return new Promise(function(resolve, reject){
  198. get(resolve, reject);
  199. });
  200. },
  201. getTargets: function(selectors, retry = 10, interval = 1*SECOND){
  202. return Promise.all(Object.values(selectors).map(selector => core.getTarget(selector, retry, interval)));
  203. },
  204. addStyle: function(name = 'style', d = document){
  205. if(html[name] === undefined) return;
  206. if(d.head){
  207. let style = createElement(html[name]()), id = SCRIPTID + '-' + name, old = d.getElementById(id);
  208. style.id = id;
  209. d.head.appendChild(style);
  210. if(old) old.remove();
  211. }
  212. else{
  213. let observer = observe(d.documentElement, function(){
  214. if(!d.head) return;
  215. observer.disconnect();
  216. core.addStyle(name);
  217. });
  218. }
  219. },
  220. };
  221. const html = {
  222. style: () => `
  223. <style type="text/css">
  224. /* maximize lyricBody */
  225. [data-selector="lyricBody"][data-columns]{
  226. width: 100vw;
  227. padding: 2em 1em 2em 2em;
  228. margin: 0;
  229. box-sizing: border-box;
  230. }
  231. /* columnize */
  232. [data-selector="lyricBody"][data-columns="1"]{columns: 1}
  233. [data-selector="lyricBody"][data-columns="2"]{columns: 2}
  234. [data-selector="lyricBody"][data-columns="3"]{columns: 3}
  235. [data-selector="lyricBody"][data-columns="4"]{columns: 4}
  236. [data-selector="lyricBody"][data-columns="5"]{columns: 5}
  237. [data-selector="lyricBody"][data-columns="6"]{columns: 6}
  238. [data-selector="lyricBody"][data-columns="7"]{columns: 7}
  239. [data-selector="lyricBody"][data-columns="8"]{columns: 8}
  240. [data-selector="lyricBody"][data-columns="9"]{columns: 9}
  241. /* no distracting elements */
  242. [data-selector="lyricBody"][data-columns] ~ *{
  243. display: none;
  244. }
  245. </style>
  246. `,
  247. 'style-google': () => `
  248. <style type="text/css">
  249. body.${SCRIPTID} [data-selector="lyricBody"]{
  250. background: white;/* i don't know why it is required on fullscreen */
  251. }
  252. body.${SCRIPTID} .OULBYb/* SO FRAGILE!! */{
  253. display: none;
  254. }
  255. body.${SCRIPTID} [role="contentinfo"]{
  256. display: block;
  257. }
  258. </style>
  259. `,
  260. 'style-azlyrics': () => `
  261. <style type="text/css">
  262. body.${SCRIPTID} [data-selector="lyricBody"]{
  263. background: rgb(221, 221, 238);/* i don't know why it is required on fullscreen */
  264. }
  265. body.${SCRIPTID} .navbar-bottom,
  266. body.${SCRIPTID} .navbar-bottom ~ div{
  267. display: block;
  268. }
  269. </style>
  270. `,
  271. 'style-genius': () => `
  272. <style type="text/css">
  273. body.${SCRIPTID} [data-selector="lyricBody"]{
  274. background: white;/* i don't know why it is required on fullscreen */
  275. }
  276. body.${SCRIPTID} .page_footer{
  277. display: block;
  278. }
  279. </style>
  280. `,
  281. 'style-lyrics': () => `
  282. <style type="text/css">
  283. body.${SCRIPTID} #main{
  284. width: 100vw;
  285. margin: 20px 0 0;
  286. max-width: 100vw;
  287. padding: 0;
  288. }
  289. body.${SCRIPTID} [data-selector="lyricBody"]{
  290. white-space: pre-wrap;
  291. font-family: 'Droid Sans',sans-serif;
  292. font-weight: 400;
  293. font-size: 18px;
  294. line-height: 26px;
  295. background: white;/* i don't know why it is required on fullscreen */
  296. }
  297. body.${SCRIPTID} footer{
  298. display: block;
  299. }
  300. </style>
  301. `,
  302. 'style-jlyric': () => `
  303. <style type="text/css">
  304. body.${SCRIPTID} [data-selector="lyricBody"]{
  305. margin: 0 !important;
  306. background: white;/* i don't know why it is required on fullscreen */
  307. }
  308. body.${SCRIPTID} #ftb{
  309. display: block;
  310. }
  311. </style>
  312. `,
  313. 'style-kget': () => `
  314. <style type="text/css">
  315. body.${SCRIPTID} [data-selector="lyricBody"]{
  316. font-size: 123.1%;
  317. font-family: "Hiragino Mincho ProN", Meiryo, "MS PMincho", serif;
  318. background: white;/* i don't know why it is required on fullscreen */
  319. }
  320. body.${SCRIPTID} [data-selector="lyricBody"] > a{
  321. display: none;
  322. }
  323. body.${SCRIPTID} #footer-wrap{
  324. display: block;
  325. }
  326. </style>
  327. `,
  328. 'style-utanet': () => `
  329. <style type="text/css">
  330. body.${SCRIPTID} [data-selector="lyricBody"]{
  331. font-size: 15px;
  332. background: white;/* i don't know why it is required on fullscreen */
  333. }
  334. body.${SCRIPTID} #footer_map,
  335. body.${SCRIPTID} #footer_bottom{
  336. display: block;
  337. }
  338. </style>
  339. `,
  340. 'style-utaten': () => `
  341. <style type="text/css">
  342. body.${SCRIPTID} footer{
  343. display: block !important;
  344. }
  345. body.${SCRIPTID} footer > aside{
  346. display: none;
  347. }
  348. body.${SCRIPTID}{
  349. background: #343330;
  350. }
  351. </style>
  352. `,
  353. };
  354. const $ = function(s, f = undefined){
  355. let target = document.querySelector(s);
  356. if(target === null) return null;
  357. return f ? f(target) : target;
  358. };
  359. const $$ = function(s, f = undefined){
  360. let targets = document.querySelectorAll(s);
  361. return f ? f(targets) : targets;
  362. };
  363. const createElement = function(html = '<div></div>'){
  364. let outer = document.createElement('div');
  365. outer.insertAdjacentHTML('afterbegin', html);
  366. return outer.firstElementChild;
  367. };
  368. const log = function(){
  369. if(typeof DEBUG === 'undefined') return;
  370. console.log(...log.build(new Error(), ...arguments));
  371. };
  372. log.build = function(error, ...args){
  373. let l = log.last = log.now || new Date(), n = log.now = new Date();
  374. let line = log.format.getLine(error), callers = log.format.getCallers(error);
  375. //console.log(error.stack);
  376. return [SCRIPTID + ':',
  377. /* 00:00:00.000 */ n.toLocaleTimeString() + '.' + n.getTime().toString().slice(-3),
  378. /* +0.000s */ '+' + ((n-l)/1000).toFixed(3) + 's',
  379. /* :00 */ ':' + line,
  380. /* caller.caller */ (callers[2] ? callers[2] + '() => ' : '') +
  381. /* caller */ (callers[1] || '') + '()',
  382. ...args
  383. ];
  384. };
  385. log.formats = [{
  386. name: 'Firefox Scratchpad',
  387. detector: /MARKER@Scratchpad/,
  388. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  389. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  390. }, {
  391. name: 'Firefox Console',
  392. detector: /MARKER@debugger/,
  393. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  394. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  395. }, {
  396. name: 'Firefox Greasemonkey 3',
  397. detector: /\/gm_scripts\//,
  398. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  399. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  400. }, {
  401. name: 'Firefox Greasemonkey 4+',
  402. detector: /MARKER@user-script:/,
  403. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 500,
  404. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  405. }, {
  406. name: 'Firefox Tampermonkey',
  407. detector: /MARKER@moz-extension:/,
  408. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 2,
  409. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  410. }, {
  411. name: 'Chrome Console',
  412. detector: /at MARKER \(<anonymous>/,
  413. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  414. getCallers: (e) => e.stack.match(/[^ ]+(?= \(<anonymous>)/gm),
  415. }, {
  416. name: 'Chrome Tampermonkey',
  417. detector: /at MARKER \(chrome-extension:.*?\/userscript.html\?name=/,
  418. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1] - 1,
  419. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  420. }, {
  421. name: 'Chrome Extension',
  422. detector: /at MARKER \(chrome-extension:/,
  423. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  424. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  425. }, {
  426. name: 'Edge Console',
  427. detector: /at MARKER \(eval/,
  428. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1],
  429. getCallers: (e) => e.stack.match(/[^ ]+(?= \(eval)/gm),
  430. }, {
  431. name: 'Edge Tampermonkey',
  432. detector: /at MARKER \(Function/,
  433. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1] - 4,
  434. getCallers: (e) => e.stack.match(/[^ ]+(?= \(Function)/gm),
  435. }, {
  436. name: 'Safari',
  437. detector: /^MARKER$/m,
  438. getLine: (e) => 0,/*e.lineが用意されているが最終呼び出し位置のみ*/
  439. getCallers: (e) => e.stack.split('\n'),
  440. }, {
  441. name: 'Default',
  442. detector: /./,
  443. getLine: (e) => 0,
  444. getCallers: (e) => [],
  445. }];
  446. log.format = log.formats.find(function MARKER(f){
  447. if(!f.detector.test(new Error().stack)) return false;
  448. //console.log('////', f.name, 'wants', 0/*the exact line number here*/, '\n' + new Error().stack);
  449. return true;
  450. });
  451. core.initialize();
  452. if(window === top) console.timeEnd(SCRIPTID);
  453. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址