SidePanel

轻小说文库++的侧边工具栏

Version vom 17.08.2022. Aktuellste Version

Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @require https://update.greasyfork.ip-ddns.com/scripts/449718/1082512/SidePanel.js

/* eslint-disable no-multi-spaces */
/* eslint-disable userscripts/no-invalid-headers */
/* eslint-disable userscripts/no-invalid-grant */

// ==UserScript==
// @name               SidePanel
// @namespace          Wenku8++
// @version            0.1.2
// @description        轻小说文库++的侧边工具栏
// @author             PY-DNG
// @license            GPL-v3
// @regurl             https?://www\.wenku8\.net/.*
// @require            https://greasyfork.runtimutd.eu.org/scripts/449412-basic-functions/code/Basic%20Functions.js?version=1080639
// @grant              none
// ==/UserScript==

(function __MAIN__() {
	'use strict';

	const tippy = require('tippy');

	exports = sideFunctions();

	// Side functions area
	function sideFunctions() {
		const SPanel = new SidePanel();
		SPanel.create();
		SPanel.setPosition('bottom-right');
		addStyle('#sidepanel-panel {background-color: #00000000;z-index: 4000;}.sidepanel-button {font-size: 1vmin;color: #1E64DC;background-color: #FDFDFD;}.sidepanel-button:hover, .sidepanel-button.low-opacity:hover {opacity: 1;color: #FDFDFD;background-color: #1E64DC;}.sidepanel-button.low-opacity{opacity: 0.4 }.sidepanel-button>i[class^="fa-"] {line-height: 3vmin;width: 3vmin;}.sidepanel-button[class*="tooltip"]:hover::after {font-size: 0.9rem;top: calc((5vmin - 25px) / 2);}.sidepanel-button[class*="tooltip"]:hover::before {top: calc((5vmin - 12px) / 2);}.sidepanel-button.accept-pointer{pointer-events:auto;}');

		commonButtons();
		return SPanel;

		function commonButtons() {
			// Button show/hide-all-buttons
			const btnShowHide = SPanel.add({
				faicon: 'fa-solid fa-down-left-and-up-right-to-center',
				className: 'accept-pointer',
				tip: '隐藏面板',
				onclick: (function() {
					let hidden = false;
					return (e) => {
						hidden = !hidden;
						btnShowHide.faicon.className = 'fa-solid ' + (hidden ? 'fa-up-right-and-down-left-from-center' : 'fa-down-left-and-up-right-to-center');
						btnShowHide.classList[hidden ? 'add' : 'remove']('low-opacity');
						btnShowHide.setAttribute('aria-label', (hidden ? '显示面板' : '隐藏面板'));
						SPanel.elements.panel.style.pointerEvents = hidden ? 'none' : 'auto';
						for (const button of SPanel.elements.buttons) {
							if (button === btnShowHide) {continue;}
							//button.style.display = hidden ? 'none' : 'block';
							button.style.pointerEvents = hidden ? 'none' : 'auto';
							button.style.opacity = hidden ? '0' : '1';
						}
					};
				}) ()
			});

			// Button scroll-to-bottom
			const btnDown = SPanel.add({
				faicon: 'fa-solid fa-angle-down',
				tip: '转到底部',
				onclick: (e) => {
					const elms = [document.body.parentElement, $('#content'), $('#contentmain')];

					for (const elm of elms) {
						elm && elm.scrollTo && elm.scrollTo(elm.scrollLeft, elm.scrollHeight);
					}
				}
			});

			// Button scroll-to-top
			const btnUp = SPanel.add({
				faicon: 'fa-solid fa-angle-up',
				tip: '转到顶部',
				onclick: (e) => {
					const elms = [document.body.parentElement, $('#content'), $('#contentmain')];

					for (const elm of elms) {
						elm && elm.scrollTo && elm.scrollTo(elm.scrollLeft, 0);
					}
				}
			});

			// Darkmode
			/*
			const btnDarkmode = SPanel.add({
				faicon: 'fa-solid ' + (DMode.isActivated() ? 'fa-sun' : 'fa-moon'),
				tip: '明暗切换',
				onclick: (e) => {
					DMode.toggle();
					btnDarkmode.faicon.className = 'fa-solid ' + (DMode.isActivated() ? 'fa-sun' : 'fa-moon');
				}
			});
			*/

			// Refresh page
			const btnRefresh = SPanel.add({
				faicon: 'fa-solid fa-rotate-right',
				tip: '刷新页面',
				onclick: (e) => {
					location.href = location.href;
				}
			});
		}
	}

    // Side-located control panel
	// Requirements: FontAwesome, tippy.js, addStyle
	// Use 'new' keyword
	function SidePanel() {
		// Public SP
		const SP = this;
		const elms = SP.elements = {};

		// Private _SP
		// keys start with '_' shouldn't be modified
		const _SP = {
			_id: {
				css: 'sidepanel-style',
				usercss: 'sidepanel-style-user',
				panel: 'sidepanel-panel'
			},
			_class: {
				button: 'sidepanel-button'
			},
			_directions: ['left', 'right', 'top', 'bottom']
		};

		addStyle('#sidepanel-panel {position: fixed; background-color: #00000000; padding: 0.5vmin; line-height: 3.5vmin; height: auto; display: flex; transition-duration: 0.3s; z-index: 9999999999;} #sidepanel-panel.right {right: 3vmin;} #sidepanel-panel.bottom {bottom: 3vmin; flex-direction: column-reverse;} #sidepanel-panel.left {left: 3vmin;} #sidepanel-panel.top {top: 3vmin; flex-direction: column;} .sidepanel-button {padding: 1vmin; margin: 0.5vmin; font-size: 3.5vmin; border-radius: 10%; text-align: center; color: #00000088; background-color: #FFFFFF88; box-shadow:3px 3px 2px #00000022; user-select: none; transition-duration: inherit;} .sidepanel-button:hover {color: #FFFFFFDD; background-color: #000000DD;}', 'sidepanel');

		SP.create = function() {
			// Create panel
			const panel = elms.panel = document.createElement('div');
			panel.id = _SP._id.panel;
			SP.setPosition('bottom-right');
			document.body.appendChild(panel);

			// Prepare buttons
			elms.buttons = [];
		}

		// Insert a button to given index
		// details = {index, text, faicon, id, tip, className, onclick, listeners}, all optional
		// listeners = [..[..args]]. [..args] will be applied as button.addEventListener's args
		// faicon = 'fa-icon-name-classname fa-icon-style-classname', this arg stands for a FontAwesome icon to be inserted inside the botton
		// Returns the button(HTMLDivElement), including button.faicon(HTMLElement/HTMLSpanElement in firefox, <i>) if faicon is set
		SP.insert = function(details) {
			const index = details.index;
			const text = details.text;
			const faicon = details.faicon;
			const id = details.id;
			const tip = details.tip;
			const className = details.className;
			const onclick = details.onclick;
			const listeners = details.listeners || [];

			const button = document.createElement('div');
			text && (button.innerHTML = text);
			id && (button.id = id);
			tip && setTooltip(button, tip); //settip(button, tip);
			className && (button.className = className);
			onclick && (button.onclick = onclick);
			if (faicon) {
				const i = document.createElement('i');
				i.className = faicon;
				button.faicon = i;
				button.appendChild(i);
			}
			for (const listener of listeners) {
				button.addEventListener.apply(button, listener);
			}
			button.classList.add(_SP._class.button);

			elms.buttons = insertItem(elms.buttons, button, index);
			index < elms.buttons.length ? elms.panel.insertBefore(button, elms.panel.children[index]) : elms.panel.appendChild(button);
			return button;
		}

		// Append a button
		SP.add = function(details) {
			details.index = elms.buttons.length;
			return SP.insert(details);
		}

		// Remove a button
		SP.remove = function(arg) {
			let index, elm;
			if (arg instanceof HTMLElement) {
				elm = arg;
				index = elms.buttons.indexOf(elm);
			} else if (typeof(arg) === 'number') {
				index = arg;
				elm = elms.buttons[index];
			} else if (typeof(arg) === 'string') {
				elm = $(elms.panel, arg);
				index = elms.buttons.indexOf(elm);
			}

			elms.buttons = delItem(elms.buttons, index);
			elm.parentElement.removeChild(elm);
		}

		// Sets the display position by texts like 'right-bottom'
		SP.setPosition = function(pos) {
			const poses = _SP.direction = pos.split('-');
			const avails = _SP._directions;

			// Available check
			if (poses.length !== 2) {return false;}
			for (const p of poses) {
				if (!avails.includes(p)) {return false;}
			}

			// remove all others
			for (const p of avails) {
				elms.panel.classList.remove(p);
			}

			// add new pos
			for (const p of poses) {
				elms.panel.classList.add(p);
			}

			// Change tooltips' direction
			elms.buttons && elms.buttons.forEach(setTooltipDirection);
		}

		// Gets the current display position
		SP.getPosition = function() {
			return _SP.direction.join('-');
		}

		// Append a style text to document(<head>) with a <style> element
		// Replaces existing id-specificed <style>s
		function spAddStyle(css, id) {
			const style = document.createElement("style");
			id && (style.id = id);
			style.textContent = css;
			for (const elm of $All('#'+id)) {
				elm.parentElement && elm.parentElement.removeChild(elm);
			}
			document.head.appendChild(style);
		}

		// Set a tooltip to the element
		function setTooltip(elm, text, direction='auto') {
			elm.tooltip = tippy(elm, {
				content: text,
				arrow: true,
				hideOnClick: false
			});

			setTooltipDirection(elm, direction);
		}

		function setTooltipDirection(elm, direction='auto') {
			direction === 'auto' && (direction = _SP.direction.includes('left') ? 'right' : 'left');
			if (!_SP._directions.includes(direction)) {throw new Error('setTooltip: invalid direction');}

			// Tippy direction
			if (!elm.tooltip) {
				DoLog(LogLevel.Error, 'SidePanel.setTooltipDirection: Given elm has no tippy instance(elm.tooltip)');
				throw new Error('SidePanel.setTooltipDirection: Given elm has no tippy instance(elm.tooltip)');
			}
			elm.tooltip.setProps({
				placement: direction
			});
		}

		// Del an item from an array using its index. Returns the array but can NOT modify the original array directly!!
		function delItem(arr, index) {
			arr = arr.slice(0, index).concat(arr.slice(index+1));
			return arr;
		}

		// Insert an item into an array using given index. Returns the array but can NOT modify the original array directly!!
		function insertItem(arr, item, index) {
			arr = arr.slice(0, index).concat(item).concat(arr.slice(index));
			return arr;
		}
	}
})();