// ==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);
})();