// ==UserScript== // @name Minimal Desktop 起始页 网页剪藏 // @namespace https://github.com/qq5855144/Minimal-Desktop // @version 1.3.2 // @description 这是一个Minimal Desktop起始页配套脚本,功能:将当前浏览的网页添加到Minimal-Desktop起始页的桌面、书签。解决网页版起始页无法使用剪藏功能的缺陷、扩展版不需要这个脚本 // @author Minimal Desktop // @match http://*/* // @match https://*/* // @exclude https://qq5855144.github.io/Minimal-Desktop/* // @grant GM_getValue // @grant GM_setValue // @run-at document-idle // ==/UserScript== (() => { 'use strict'; const DESKTOP_URL = 'https://qq5855144.github.io/Minimal-Desktop/'; const POSITION_KEY = 'minimal-desktop-clipper-position-v1'; const IDLE_DELAY = 2000; const BUTTON_EDGE_MARGIN = 20; const MAX_URL_LENGTH = 8192; const MAX_ICON_LENGTH = 4096; const rootUrl = new URL(DESKTOP_URL); if (location.origin === rootUrl.origin && location.pathname.startsWith(rootUrl.pathname)) return; if (!/^https?:$/.test(location.protocol) || location.href.length > MAX_URL_LENGTH) return; const readValue = (key, fallback) => { try { if (typeof GM_getValue === 'function') return GM_getValue(key, fallback); } catch (_) {} try { const value = localStorage.getItem(key); return value === null ? fallback : JSON.parse(value); } catch (_) { return fallback; } }; const writeValue = (key, value) => { try { if (typeof GM_setValue === 'function') { GM_setValue(key, value); return; } } catch (_) {} try { localStorage.setItem(key, JSON.stringify(value)); } catch (_) {} }; const extractSiteName = (raw) => { const trimmed = raw.trim(); const firstSeparator = trimmed.match(/^(.+?)\s*[-|–—,·\/]\s*.+$/); let name = firstSeparator ? firstSeparator[1].trim() : trimmed; const decoratedSuffix = name.match(/^(.+?)\s+(?=[(\[{<(【《『「'"@#$%^&*~`!?。,、;:☆★♪♫♬♩©®™°•])/u); if (decoratedSuffix) name = decoratedSuffix[1].trim(); return name.length > 64 ? `${name.slice(0, 63)}…` : name; }; const findFavicon = () => { const links = [...document.querySelectorAll('link[rel~="icon"]')]; const href = links.at(-1)?.href || `${location.origin}/favicon.ico`; return /^https?:\/\//i.test(href) && href.length <= MAX_ICON_LENGTH ? href : ''; }; const host = document.createElement('div'); host.id = 'minimal-desktop-clipper-host'; host.style.cssText = 'all:initial;position:fixed;right:0;top:50%;z-index:2147483647;width:176px;height:40px;pointer-events:none'; const shadow = host.attachShadow({ mode: 'closed' }); shadow.innerHTML = `
`; const panel = shadow.querySelector('.clipper'); const dragHandle = shadow.querySelector('.drag-handle'); const image = shadow.querySelector('img'); const fallback = shadow.querySelector('.fallback'); const favicon = findFavicon(); const showFallback = () => { image.style.display = 'none'; fallback.style.display = 'grid'; }; if (favicon) { image.addEventListener('error', showFallback, { once: true }); image.src = favicon; } else showFallback(); const clampRatio = (value) => Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 0.5; let positionRatio = clampRatio(Number(readValue(POSITION_KEY, 0.5))); const applyPosition = () => { const minY = Math.min(BUTTON_EDGE_MARGIN, window.innerHeight / 2); const maxY = Math.max(minY, window.innerHeight - BUTTON_EDGE_MARGIN); const centerY = Math.min(maxY, Math.max(minY, positionRatio * window.innerHeight)); host.style.transform = `translateY(${centerY - window.innerHeight / 2 - 20}px)`; }; applyPosition(); window.addEventListener('resize', applyPosition, { passive: true }); let idleTimer; let isClipperExtended = true; const extend = () => { if (!isClipperExtended) { isClipperExtended = true; panel.classList.add('extend'); } clearTimeout(idleTimer); }; const retract = () => { isClipperExtended = false; panel.classList.remove('extend'); }; const scheduleRetract = (delay = IDLE_DELAY) => { clearTimeout(idleTimer); idleTimer = setTimeout(retract, delay); }; panel.addEventListener('pointerenter', (event) => { if (event.pointerType !== 'touch') extend(); }); panel.addEventListener('pointerleave', () => scheduleRetract()); panel.addEventListener('focusin', extend); let startY = 0; let startCenterY = 0; let dragging = false; let suppressClick = false; let canActivateThisClick = false; panel.addEventListener('pointerdown', (event) => { if (event.button !== 0 && event.pointerType !== 'touch') return; canActivateThisClick = isClipperExtended; extend(); if (!event.target.closest?.('.drag-handle')) return; startY = event.clientY; startCenterY = positionRatio * window.innerHeight; dragging = false; panel.setPointerCapture?.(event.pointerId); }); panel.addEventListener('pointermove', (event) => { if (!panel.hasPointerCapture?.(event.pointerId)) return; const delta = event.clientY - startY; if (Math.abs(delta) > 5) dragging = true; if (!dragging) return; const minY = Math.min(BUTTON_EDGE_MARGIN, window.innerHeight / 2); const maxY = Math.max(minY, window.innerHeight - BUTTON_EDGE_MARGIN); const centerY = Math.min(maxY, Math.max(minY, startCenterY + delta)); positionRatio = window.innerHeight ? centerY / window.innerHeight : 0.5; applyPosition(); }); const finishDrag = (event) => { if (!panel.hasPointerCapture?.(event.pointerId)) return; panel.releasePointerCapture?.(event.pointerId); if (dragging) { writeValue(POSITION_KEY, positionRatio); suppressClick = true; setTimeout(() => { suppressClick = false; }, 0); } dragging = false; scheduleRetract(); }; panel.addEventListener('pointerup', finishDrag); panel.addEventListener('pointercancel', finishDrag); const confirmExtension = () => { extend(); scheduleRetract(4000); }; const openHome = () => { window.open(DESKTOP_URL, '_blank', 'noopener,noreferrer'); retract(); }; const openClip = (target) => { const payload = { url: location.href, title: extractSiteName(document.title) || location.hostname, favicon: findFavicon() || undefined, target, }; const destination = `${DESKTOP_URL}#clip=${encodeURIComponent(JSON.stringify(payload))}`; window.open(destination, '_blank', 'noopener,noreferrer'); retract(); }; panel.addEventListener('click', (event) => { if (suppressClick) return; const homeButton = event.target.closest?.('[data-action="home"]'); const targetButton = event.target.closest?.('[data-target]'); const pointerActivation = event.detail !== 0; if (!isClipperExtended || (pointerActivation && !canActivateThisClick)) { confirmExtension(); return; } if (homeButton) openHome(); else if (targetButton) openClip(targetButton.dataset.target); else scheduleRetract(4000); }); dragHandle.addEventListener('keydown', (event) => { if (event.key === 'Enter' || event.key === ' ') extend(); }); (document.body || document.documentElement).append(host); scheduleRetract(3000); })();