您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
多种增强体验的界面优化
当前为
// ==UserScript== // @name Iwara Enhancement // @name:zh-CN Iwara增强 // @namespace https://github.com/guansss/userscripts // @version 0.6 // @description Multiple UI enhancements for better experience. // @description:zh-CN 多种增强体验的界面优化 // @author guansss // @match *://*.iwara.tv/* // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/video.min.js#sha256=9HM07Of11yw3TL/m0BxP9pw08qXmG/xOTDc1d3sp2Wo= // @resource vjs-css https://cdn.jsdelivr.net/npm/[email protected]/dist/video-js.min.css#sha256=/fXfq3QrnWyMYmF0zX6ImdI1DTraNCAq1vPofa2rs2w= // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_getResourceText // @grant GM_download // @grant GM_info // @grant unsafeWindow // @run-at document-start // ==/UserScript== const VIDEOJS_THUMB_PLUGIN = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/index.min.js'; // the storage keys const KEY_VOLUME = 'volume'; const KEY_FILENAME = 'filename'; const KEY_DARK_MODE = 'dark'; const KEY_LIKE_RATES = 'like_rates'; const DEFAULT_FILENAME_TEMPLATE = 'DATE TITLE - AUTHOR (ID)'; let filenameTemplate = GM_getValue(KEY_FILENAME, DEFAULT_FILENAME_TEMPLATE); function main() { 'use strict'; const ready = new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve)); // jQuery is available only when ready ready.then(() => window.$ = unsafeWindow.$ = unsafeWindow.jQuery); { general(); enhanceList(); if (location.pathname.match(/(videos|images)\//)) { prettifyContentPage(); if (location.pathname.includes('videos')) { enhanceVideo(); setupThumbnails(); setupAutoDownload(); } } else if (location.pathname.includes('search')) { enhanceSearch(); } } async function general() { GM_addStyle(GLOBAL_STYLES); // dark mode if (GM_getValue(KEY_DARK_MODE, false)) { document.documentElement.classList.add('dark'); } await ready; // remove R18 warning $('#r18-warning').remove(); $('<a class="btn btn-info btn-sm" title="Dark mode"><i class="glyphicon glyphicon-eye-open"></i></a>') .insertAfter('#user-links .search-link') .on('click', () => { document.documentElement.classList.toggle('dark'); GM_setValue(KEY_DARK_MODE, document.documentElement.classList.contains('dark')); }); } async function enhanceList() { await ready; const likeRatesEnabled = GM_getValue(KEY_LIKE_RATES, true); $('#block-mainblocks-sub-menu .list-inline') .after(`<label for="check-like-rates" class="checkbox"><input type="checkbox" id="check-like-rates" ${likeRatesEnabled ? 'checked' : ''}>Display like rates</label>`); $('#check-like-rates').change(function() { GM_setValue(KEY_LIKE_RATES, this.checked); enableLikeRates(this.checked); }); function enableLikeRates(enabled) { if (enabled) { $('body').removeClass('hide-like-rates'); } else { $('body').addClass('hide-like-rates'); } } enableLikeRates(likeRatesEnabled); // iterates over video/image items in the page $('.view-content .views-column, .view-content .col-sm-3').each(function() { const thiz = $(this); if (thiz.children(':first-child').is('.node-teaser, .node-sidebar_teaser')) { const url = thiz.find('.title a').attr('href'); // set up like rates and highlights const viewsIcon = thiz.find('.likes-icon.left-icon'); const likesIcon = thiz.find('.likes-icon.right-icon'); // ensure the likes icon exists because it will be missing if the likes are 0 if (likesIcon.length) { let [views, likes] = [viewsIcon, likesIcon].map(icon => { let value = icon.html().replace(/<i.*<\/i>/m, '').trim(); value = value.includes('k') ? value.slice(0, -1) * 1000 : value; return +value; }); const likeRatePercent = views === 0 ? 0 : Math.round(1000 * likes / views) / 10; viewsIcon.after(`<div class="like-rate left-icon">${likeRatePercent}%</div>`); if (likeRatePercent >= 4) { thiz.addClass('highlight'); } } // differentiate images from videos in subscriptions page // by adding an "image" icon on the image item that's not denoted by a "multiple" icon if (location.href.includes('subscriptions') && !thiz.find('.multiple-icon').length) { if (url.startsWith('/images')) { viewsIcon.before('<div class="left-icon"><i class="glyphicon glyphicon-picture"></i></div>'); } } // fix broken preview images const placeholder = `<a href="${url}" class="preview-placeholder"><div>NO PREVIEW</div></a>`; const teaserContainer = thiz.find('.field-type-video .field-item'); if (!teaserContainer.children().length) { teaserContainer.append(placeholder); } else { teaserContainer.find('img').error(function() { teaserContainer.empty().append(placeholder); }); } } }); } async function enhanceVideo() { // load CSS of the new videojs GM_addStyle(GM_getResourceText('vjs-css')); // patch the player.on() to return itself to support method chaining, which is no longer supported in the new version const Player = videojs.getComponent('Player'); const readyFn = Player.prototype.ready; Player.prototype.ready = function() { const onFn = this.on; if (onFn && !onFn.patched) { this.on = function() { onFn.apply(this, arguments); return this; }; this.on.patched = true; } return readyFn.apply(this, arguments); }; // copy the plugins if the old videojs has already been loaded before this userscript is injected to the page if (unsafeWindow.videojs) { const oldVideojs = unsafeWindow.videojs; unsafeWindow.videojs = videojs; // registered plugins can be found by checking the <script> tags in page HTML const registeredPlugins = ['hotkeys', 'persistvolume', 'loopbutton', 'videoJsResolutionSwitcher']; // copy plugins to the new videojs for (const plugin of registeredPlugins) { const pluginMethod = oldVideojs.getComponent('Player').prototype[plugin]; if (typeof pluginMethod === 'function') { videojs.registerPlugin(plugin, pluginMethod); } } } // otherwise, prevent the old videojs from loading else { unsafeWindow.videojs = videojs; let scriptExists = false; // there's a chance that the <script> tag of videojs has been inserted to the page, // I'm not quite sure though for (const element of document.head.children) { if (element.src && element.src.includes('video-js/video.js')) { element.remove(); scriptExists = true; break; } } if (!scriptExists) { // immediately remove the <script> tag once it's inserted to the HTML new MutationObserver((mutationsList, observer) => { mutationsList.forEach(mutation => { if (mutation.type === 'childList') { for (const node of mutation.addedNodes) { if (node && node.src && node.src.includes('video-js/video.js')) { observer.disconnect(); node.remove(); } } } }); }).observe(document.head, { childList: true }); } } // recover the volume in incognito mode if (localStorage['-volume'] === undefined) { localStorage['-volume'] = GM_getValue(KEY_VOLUME, 0.5); } await ready; // remove CSS of the old videojs for (const node of document.head.childNodes) { if (node && node.tagName === 'STYLE' && node.innerHTML.includes('video-js')) { node.innerHTML = node.innerHTML.replace(/.+?video-js\.min\.css.+/, ''); } } const player = await repeatUntil(() => videojs.getPlayers()['video-player']); player .on('fullscreenchange', () => { $('#video-player').focus(); }) .on('volumechange', () => { // save volume when changed GM_setValue(KEY_VOLUME, player.volume() || 0.5); }); } async function setupThumbnails() { await ready; const player = await repeatUntil(() => videojs.getPlayers()['video-player']); // e.g. //i.iwara.tv/sites/default/files/videos/thumbnails/1404656/thumbnail-1404656_0001.jpg const previewURL = player.poster(); if (previewURL && previewURL.includes('thumbnail')) { // the thumbnail plugin is a commonjs module so we have to define these stuff for it unsafeWindow.exports = {}; unsafeWindow.require = module => module === 'video.js' ? videojs : undefined; // load the thumbnail plugin await new Promise(resolve => { const script = document.createElement('script'); script.onload = resolve; script.src = VIDEOJS_THUMB_PLUGIN; document.head.appendChild(script); }); // duration and dimensions are included in the meta data player.on('loadedmetadata', () => { const division = 16; const interval = player.duration() / division; const width = 180; const height = width * player.videoHeight() / player.videoWidth(); // strip the image number as well as the extension const thumbBaseURL = previewURL.slice(0, -6); const sprites = []; for (let i = 0; i < division - 1; i++) { // append the base URL with numbers, starting from 01 const url = thumbBaseURL + (i + 1 + '').padStart(2, '0') + '.jpg'; // using (division-1) thumbnails to cover all the segments // // // thumbs(index): 0 1 2 3 div-3 div-2 // v v v v v v // timeline: +-------+-------+-------+-------+-- ... ------+-------+-------+ // ^ ^ ^ ^ ^ ^ ^ // time spans: |___________|_______|_______|______ ... __|_______|___________| // 0 1 2 3 div-3 div-2 let start, timeSpan; switch (i) { case 0: start = 0; timeSpan = interval * 1.5; break; case division - 2: start = interval * (0.5 + i); // add extra 0.1 due to the floating point computation... timeSpan = interval * (1.5 + 0.1); break; default: start = interval * (0.5 + i); timeSpan = interval; } sprites.push({ url, width, height, start, duration: timeSpan, interval: timeSpan, }); } player.thumbnailSprite({ sprites }); }); } } async function prettifyContentPage() { await ready; // show full description $('.field-name-body a.show').click(); // enlarge content area $('.node-full .col-sm-12:last-child').removeClass('col-sm-12').addClass('col-sm-9').parent().append($('.container .sidebar')); $('.container>.col-sm-9, .node-full').removeClass('col-sm-9').addClass('col-sm-12'); // $('.extra-content-block').remove(); // move "liked by" block to the bottom $('#block-views-likes-block').appendTo('.sidebar .region-sidebar'); } async function setupAutoDownload() { await ready; // wait for Bootstrap's initialization await delay(200); function getDownloadTarget(template = filenameTemplate) { const url = $('#download-options li:first-child a')[0].href; const ext = url.match(/Source(\.[^&]+)/)[1]; const urlMatches = unescape(url).match(/file=.+\/(\d+)_(\w+)_/); const uploadDate = urlMatches[1] * 1000; const id = urlMatches[2]; const title = $('.node-info .title').text(); const author = $('.node-info .username').text(); const vars = { ID: id, TITLE: title, AUTHOR: author, DATE: formatDate(new Date()), DATE_TS: new Date(), UP_DATE: formatDate(new Date(uploadDate)), UP_DATE_TS: uploadDate, }; // the keys should be sorted to prevent certain keys from overriding its longer form // e.g. "DATE_TS" gets populated with DATE instead of DATE_TS const sortedKeys = Object.keys(vars).sort((a, b) => b.length - a.length); const filename = sortedKeys.reduce((_filename, key) => _filename.replace(key, vars[key]), template) // strip characters disallowed in file path .replace(/[*/:<>?\\|]/g, ''); return { url, filename: filename + ext, }; } const downloadBtn = $('#download-button'); const downloadBtnHTML = downloadBtn.html(); // a function to abort the current download let abortDownload; unsafeWindow.onbeforeunload = () => { if (abortDownload) { // the message is unlikely to be displayed in modern browsers but, just in case return 'Download still in progress, would you like to abort it and exit?'; } }; unsafeWindow.onunload = () => abortDownload && abortDownload(); if (GM_info.downloadMode !== 'disabled') { downloadBtn.off('click').click(function(e) { try { e.preventDefault(); this.blur(); const likeBtn = $('.flag-like a'); // like button exists if user has logged in if (likeBtn.length) { // like the video if not liked if (!likeBtn.attr('href').includes('unflag')) { likeBtn.click(); } } const downloadTarget = getDownloadTarget(); downloadBtn.addClass('btn-disabled'); let onprogress; // progress is only available in the "native" mode if (GM_info.downloadMode !== 'browser') { onprogress = (e) => { const progress = ~~(e.loaded / e.total * 100); downloadBtn.html(downloadBtnHTML + ' ' + progress + '%'); }; onprogress({ loaded: 0, total: 1 }); } const { abort } = GM_download({ url: downloadTarget.url, name: downloadTarget.filename, saveAs: true, onload: downloadEnded, onerror: downloadEnded, ontimeout: downloadEnded, onprogress, }); // aborting will be handled by the browser's download manager in non-native mode if (GM_info.downloadMode === 'native') { abortDownload = abort; } } catch (e) { showError(e + ''); } }); } else { showError(new Error('Download has been disabled, the default method will be used instead.')); } function downloadEnded(e) { if (e && e.error) { console.warn('Download error', e); showError(`Download error (${e.error}): ${e.details.current}`); } downloadBtn.removeClass('btn-disabled').html(downloadBtnHTML); abortDownload = undefined; } function showError(msg) { $('#download-options').before(`<div class="text-danger">${msg}</div>`); } $('<a id="options-switch" class="icon-btn glyphicon glyphicon-cog"></a>') .insertAfter('#download-button') .click(() => { $('#download-options').toggleClass('hidden'); $('#filename-input').trigger('input'); }); $(` <div class="page-node-edit"> <h3>Download filename</h3> <p>The filename template to use when downloading a video.</p> <p>Note the userscript settings will be lost when exiting the incognito mode, so in order to apply the settings permanently, you need to modify them in non-incognito mode.</p> <p>If you're using Tampermonkey, you can check the <a href="https://greasyfork.org/scripts/416003-iwara-enhancement">description</a> for how to improve the download experience.</p> <pre>ID the video's ID TITLE title AUTHOR author's name DATE date time when the download starts DATE_TS the DATE in timestamp format UP_DATE date time when the video was uploaded UP_DATE_TS the UP_DATE in timestamp format</pre> <input type="text" id="filename-input" class="form-text" value="${filenameTemplate}"> <a id="filename-submit" class="icon-btn glyphicon glyphicon-ok" title="Apply"></a> <a id="filename-reset" class="icon-btn glyphicon glyphicon-repeat" title="Reset to default"></a> <p id="filename-preview"></p> </div>`) .prependTo('#download-options .panel-body'); $('#filename-input').on('input', function(e) { $('#filename-preview').text(getDownloadTarget(this.value).filename); const isChanged = this.value !== filenameTemplate; const isDefault = this.value === DEFAULT_FILENAME_TEMPLATE; $('#filename-submit')[isChanged ? 'show' : 'hide'](); $('#filename-reset')[!isDefault ? 'show' : 'hide'](); }); $('#filename-submit').hide().click(() => { filenameTemplate = $('#filename-input').val(); GM_setValue(KEY_FILENAME, filenameTemplate); $('#filename-input').trigger('input'); }); $('#filename-reset').hide().click(() => { $('#filename-input').val(DEFAULT_FILENAME_TEMPLATE); $('#filename-submit').click(); }); } async function enhanceSearch() { await ready; $('.node-image').each(function() { const thiz = $(this); const twitterShareLink = thiz.find('[title="Share on Twitter"]').attr('href'); if (twitterShareLink) { let iwaraLink = twitterShareLink.slice(twitterShareLink.indexOf('http', 10)); iwaraLink = decodeURIComponent(iwaraLink); thiz.find('h1').wrapInner(`<a href="${iwaraLink}"></a>`); } }); } function repeat(fn, interval = 200) { if (fn()) { return 0; } const id = setInterval(() => { try { fn() && clearInterval(id); } catch (e) { clearInterval(id); } }, interval); return id; } // non-cancelable function repeatUntil(fn, interval) { return new Promise(resolve => repeat(() => { const result = fn(); if (result) { resolve(result); return true; } }, interval)); } function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function formatDate(date) { const pad = num => String(num).padStart(2, '0'); return [ date.getFullYear(), date.getMonth() + 1, date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds(), ] .map(pad).join(''); } } // language=CSS const GLOBAL_STYLES = ` /* ============================= large screen mode ============================= */ @media (min-width: 2000px) { .container { width: 1984px; } .slick-slider { height: 920px !important; } .slick-list img { width: 1800px; } .comment .user-avatar { width: 8.33333333%; } } @media (min-width: 3000px) { .container { width: 2976px; } } /* ============================= dark mode ============================= */ .dark { background-color: #222; color: #F8F8F8; } .dark li a.active { color: #02e8bb; } .dark body, .dark footer, .dark .panel, .dark section#content > .container, .dark .node.node-full.node-video .node-info, .dark .node.node-full.node-image .node-info, .dark tr.even, .dark tr.odd, .dark .table-striped > tbody > tr:nth-child(odd) > td, .dark .table-striped > tbody > tr:nth-child(odd) > th { background-color: inherit; } .dark .node.node-full .node-buttons, .dark .node.node-full .field-name-body a.show, .dark .node.node-full .field-name-field-categories .field-items .field-item, .dark .node.node-full .field-name-field-image-categories .field-items .field-item, .dark .panel-default > .panel-heading, .dark .panel-default > .panel-footer, .dark .table-striped > tbody > tr:nth-child(even) > td, .dark .table-striped > tbody > tr:nth-child(even) > th, .dark .views-field.views-field-last-updated.active, .dark .privatemsg-header-lastupdated.active, .dark .page-messages #privatemsg-list-form tr, .dark .page-messages .private-message .message.mine, .dark .view-profile.view-display-id-block, .dark .view-id-content table > tbody > tr:nth-child(odd) > td, .dark .view-id-content table > tbody > tr:nth-child(odd) > th, .dark table.sticky-header, .dark .well, .dark .jumbotron, .dark select option { background-color: #2a2a2a; } .dark .page-messages .private-message .message.theirs, .dark .view-profile.view-display-id-block .views-field-field-about { background-color: #444; } .dark .page-node-add .form-textarea, .dark .page-node-edit .form-textarea, .dark .page-node-add .form-text, .dark .page-node-edit .form-text, .dark pre, .dark select, .dark textarea, .dark input:not(.btn):not(.form-submit) { background-color: rgba(255, 255, 255, .05); } .dark .view-profile.view-display-id-block .views-field-field-about, .dark .panel-default, .dark .panel-default > .panel-heading, .dark .panel-default > .panel-footer, .dark .well, .dark pre, .dark textarea, .dark input[type="text"], .dark table, .dark thead, .dark tbody, .dark tfoot, .dark tr, .dark th, .dark td { border-color: #333 !important; } .dark h1, .dark h2, .dark h3, .dark h4, .dark h5, .dark h6 { border-color: #666 !important; } .dark body, .dark .node.node-teaser h3.title a, .dark .panel-default > .panel-heading, .dark .page-node-add .form-textarea, .dark .page-node-edit .form-textarea, .dark .page-node-add .form-text, .dark .page-node-edit .form-text { color: inherit; } /* ============================= item list ============================= */ #block-mainblocks-sub-menu .list-inline { display: inline-block; } #block-mainblocks-sub-menu .checkbox { display: inline-block; margin: 0 16px; font-size: inherit; } /* hide likes icons only when displaying like rates */ .hide-like-rates .like-rate, body:not(.hide-like-rates) .likes-icon.left-icon { margin: 0; width: 0; overflow: hidden; } .preview-placeholder { position: relative; display: block; padding-bottom: calc(100% * 150 / 220); /* numbers from the width and height attributes of preview images */ } .preview-placeholder > * { position: absolute; top: 0; right: 0; bottom: 0; left: 0; display: flex; justify-content: center; align-items: center; color: #888; font-size: 1.5em; text-align: center; line-height: 1.2; background: rgba(128, 128, 128, .1); } .highlight { background-color: #79ecd6; } .highlight .username { color: #555; } .highlight .preview-placeholder > * { color: #EEE; } .dark .highlight { background-color: #048c72; } .dark .highlight .username { color: #CCC; } /* ============================= progress bar thumbnails ============================= */ .vjs-mouse-display .vjs-time-tooltip { background-size: cover; text-shadow: 0 0 2px black, 0 0 2px black !important; } /* ============================= auto-download options ============================= */ .btn-disabled { opacity: 0.7; pointer-events: none; } .icon-btn { margin-left: 4px; padding: 8px 8px; cursor: pointer; } #options-switch { vertical-align: middle; } #filename-input { margin-top: 2px; width: 400px; max-width: 100%; } #filename-preview { color: #777; font-size: 0.8em; } /* ============================= misc ============================= */ video { outline: none !important; } `; main();