輕小說文庫 wenku8 字型+大小調整+去除空行

即時調整 Wenku8 內容區字型(自定義)與大小,並可一鍵去除多餘空行,狀態自動記憶

// ==UserScript==
// @name         輕小說文庫 wenku8 字型+大小調整+去除空行
// @namespace    http://tampermonkey.net/
// @version      1.8.1
// @description  即時調整 Wenku8 內容區字型(自定義)與大小,並可一鍵去除多餘空行,狀態自動記憶
// @author       shanlan(ChatGPT o3-mini)
// @match        http*://www.wenku8.net/modules/article/reader.php*cid=*
// @match        http*://www.wenku8.cc/modules/article/reader.php*cid=*
// @match        http*://www.wenku8.net/novel/*
// @match        http*://www.wenku8.cc/novel/*
// @grant        none
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function(){
"use strict";
const uiStyle = document.createElement("style");
uiStyle.textContent = `
.tm-ui-btn, .tm-ui-panel{
  all:unset;
  box-sizing:border-box;
  font-family:sans-serif;
}
.tm-ui-btn{
  position:fixed;
  width:48px;         /* 調整前為 32px */
  height:48px;        /* 調整前為 32px */
  background:#333 !important;
  color:#f1f1f1 !important;
  border:1px solid #555 !important;
  border-radius:50% !important;
  box-shadow:0 2px 8px rgba(0,0,0,0.5) !important;
  display:flex !important;
  align-items:center !important;
  justify-content:center !important;
  font-weight:bold !important;
  font-size:24px !important; /* 調整前為 18px */
  cursor:pointer !important;
  z-index:9999 !important;
  user-select:none !important;
  touch-action:none !important;
}
.tm-ui-panel{
  position:fixed;
  background:#222 !important;
  color:#f1f1f1 !important;
  border:1px solid #555 !important;
  border-radius:12px !important;
  box-shadow:0 2px 12px rgba(0,0,0,0.6) !important;
  padding:12px 16px !important; /* 調整前為 9px 10px */
  font-size:24px !important;      /* 調整前為 18px */
  display:none;
  z-index:9999 !important;
  min-width:480px;    /* 調整前為 320px */
}
.tm-ui-panel select,
.tm-ui-panel input,
.tm-ui-panel button{
  background:#333 !important;
  color:#f1f1f1 !important;
  border:1px solid #555 !important;
  border-radius:6px !important;
  padding:0px 4px !important;
  font-size:26px !important;  /* 調整前為 20px */
  margin-right:8px;
}
.tm-ui-panel button{
  cursor:pointer !important;
  min-width:48px;
  min-height:48px;
}
.tm-ui-panel label,
.tm-ui-panel span{
  color:#f1f1f1 !important;
  font-size:28px !important; /* 調整前為 21px */
}
.tm-ui-panel input[type="text"]{
  width:80px !important;   /* 調整前為 60px */
  text-align:center;
}
`;
document.head.appendChild(uiStyle);
const builtInFonts = [
  ["","預設"],
  ["\"Noto Sans TC\", \"思源黑體\", \"Microsoft JhengHei\", \"微軟正黑體\", sans-serif","思源黑體"],
  ["\"Noto Serif TC\", \"思源宋體\", \"PMingLiU\", \"新細明體\", serif","思源宋體"],
  ["\"Microsoft JhengHei\", \"微軟正黑體\", sans-serif","微軟正黑體"],
  ["\"Microsoft JhengHei UI\", \"微軟正黑體 UI\", sans-serif","微軟正黑體 UI"],
  ["\"PMingLiU\", \"新細明體\", serif","新細明體"],
  ["\"MingLiU\", \"細明體\", serif","細明體"],
  ["\"DFKai-SB\", \"標楷體\", serif","標楷體"],
  ["\"SimSun\", \"宋體\", serif","宋體"],
  ["\"Microsoft YaHei\", \"微軟雅黑\", sans-serif","微軟雅黑"],
  ["Arial, Helvetica, sans-serif","Arial"],
  ["serif","Serif"]
];
let customFonts = [];
try{customFonts = JSON.parse(localStorage.getItem("wenku8_customFonts")||"[]");}catch(e){customFonts = [];}
let font = localStorage.getItem("wenku8_font") || "";
let size = parseInt(localStorage.getItem("wenku8_fontsize")) || 18;
function updateStyle(){
  let s = document.getElementById("wenku8-style");
  if(!s){s = document.createElement("style"); s.id="wenku8-style"; document.head.appendChild(s);}
  s.textContent = `#content, #content * { font-family: ${font||"inherit"} !important; font-size: ${size}px !important; }`;
}
updateStyle();
const mainBtn = document.createElement("div");
mainBtn.textContent = "字";
mainBtn.classList.add("tm-ui-btn");
const panel = document.createElement("div");
panel.classList.add("tm-ui-panel");
const fontLabel = document.createElement("label");
fontLabel.textContent = "字型:";
fontLabel.style.marginRight = "8px";
const fontSel = document.createElement("select");
fontSel.style.marginRight = "20px";
function updateFontOptions(){
  while(fontSel.options.length > 0) fontSel.remove(0);
  const addOption = (arr)=>arr.forEach(([v,n])=>{
    const opt = new Option(n,v);
    if(v===font) opt.selected = true;
    fontSel.add(opt);
  });
  addOption(builtInFonts);
  if(customFonts.length>0) addOption(customFonts);
}
updateFontOptions();
const btnAddFont = document.createElement("button");
btnAddFont.textContent = "+";
const btnDelFont = document.createElement("button");
btnDelFont.textContent = "-";
const sizeContainer = document.createElement("span");
sizeContainer.style.display = "inline-flex";
sizeContainer.style.alignItems = "center";
const sizeLabel = document.createElement("span");
sizeLabel.textContent = "大小:";
sizeLabel.style.marginRight = "8px";
const otherLabel = document.createElement("span");
otherLabel.textContent = "其他:";
otherLabel.style.marginRight = "8px";
const btnMinus = document.createElement("button");
btnMinus.textContent = "-";
Object.assign(btnMinus.style,{width:"36px",height:"36px",fontSize:"22px",marginRight:"8px"});
const sizeInput = document.createElement("input");
Object.assign(sizeInput,{type:"text",value:size});
sizeInput.style.width = "60px";
sizeInput.style.textAlign = "center";
sizeInput.style.marginRight = "8px";
const btnPlus = document.createElement("button");
btnPlus.textContent = "+";
Object.assign(btnPlus.style,{width:"36px",height:"36px",fontSize:"22px"});
sizeContainer.append(sizeLabel,btnPlus,sizeInput,btnMinus);
const divider = document.createElement("hr");
Object.assign(divider.style,{margin:"8px 0",border:"0",borderTop:"1px solid #555"});
const divider2 = document.createElement("hr");
Object.assign(divider2.style,{margin:"8px 0",border:"0",borderTop:"1px solid #555"});
const btnRemoveBr = document.createElement("button");
let removed = localStorage.getItem("wenku8_removebr") === "1";
btnRemoveBr.textContent = removed ? "恢復空行" : "去除空行";
panel.append(fontLabel,fontSel,btnAddFont,btnDelFont,divider,sizeContainer,divider2,otherLabel,btnRemoveBr);
let originalHTML = null;
function removeBr(){
  const c = document.querySelector("#acontent") || document.querySelector("#content");
  if(!c) return;
  if(originalHTML === null) originalHTML = c.innerHTML;
  c.innerHTML = c.innerHTML.replace(/(?:<br\s*\/?>\s*){2,}/gi,"<br>");
  btnRemoveBr.textContent = "恢復空行";
  removed = true;
  localStorage.setItem("wenku8_removebr","1");
}
function restoreBr(){
  const c = document.querySelector("#acontent") || document.querySelector("#content");
  if(!c) return;
  if(originalHTML !== null) c.innerHTML = originalHTML;
  btnRemoveBr.textContent = "去除空行";
  removed = false;
  localStorage.setItem("wenku8_removebr","0");
}
btnRemoveBr.onclick = function(){ !removed ? removeBr() : restoreBr(); };
setTimeout(()=>{
  const c = document.querySelector("#acontent") || document.querySelector("#content");
  if(!c)return;
  if(removed){
    if(originalHTML===null) originalHTML = c.innerHTML;
    c.innerHTML = c.innerHTML.replace(/(?:<br\s*\/?>\s*){2,}/gi,"<br>");
    btnRemoveBr.textContent = "恢復空行";
  }
},0);
function updateAll(){
  font = fontSel.value;
  if(sizeInput.value.trim()==="") return;
  let num = parseInt(sizeInput.value,10);
  if(num <= 0) num = 18;
  size = num;
  sizeInput.value = size;
  localStorage.setItem("wenku8_font",font);
  localStorage.setItem("wenku8_fontsize",size);
  updateStyle();
}
fontSel.addEventListener("change",updateAll);
sizeInput.addEventListener("input",updateAll);
btnMinus.addEventListener("click",()=>{ size = Math.max(1, size-2); sizeInput.value = size; updateAll(); });
btnPlus.addEventListener("click",()=>{ size = size+2; sizeInput.value = size; updateAll(); });
btnAddFont.onclick = function(){
  const newFontValue = prompt('請輸入字體代碼,例如 "Comic Sans MS", cursive, sans-serif');
  if(!newFontValue)return;
  const newFontName = prompt("請輸入字體顯示名稱");
  if(!newFontName)return;
  customFonts.push([newFontValue,newFontName]);
  localStorage.setItem("wenku8_customFonts",JSON.stringify(customFonts));
  updateFontOptions();
};
btnDelFont.onclick = function(){
  const selVal = fontSel.value;
  const foundIndex = customFonts.findIndex(item=>item[0]===selVal);
  if(foundIndex===-1){ alert("無法刪除內建字體!"); return; }
  if(confirm("確定刪除選定的自訂字體?")){
    customFonts.splice(foundIndex,1);
    localStorage.setItem("wenku8_customFonts",JSON.stringify(customFonts));
    if(font===selVal){ font = ""; localStorage.setItem("wenku8_font",""); }
    updateFontOptions();
    updateStyle();
  }
};
const MARGIN = 8, THRESHOLD = 4;
function setBtnPosition(l,t){ mainBtn.style.left = l+"px"; mainBtn.style.top = t+"px"; }
function clampBtnInView(){
  const vw = window.innerWidth, vh = window.innerHeight;
  const bw = mainBtn.offsetWidth || 32, bh = mainBtn.offsetHeight || 32;
  let l = parseFloat(mainBtn.style.left)||0, t = parseFloat(mainBtn.style.top)||0;
  l = Math.min(Math.max(l,MARGIN),vw-bw-MARGIN);
  t = Math.min(Math.max(t,MARGIN),vh-bh-MARGIN);
  setBtnPosition(l,t);
}
function measurePanelSize(){
  const pd = panel.style.display, pv = panel.style.visibility;
  panel.style.visibility="hidden";
  panel.style.display="block";
  const r = panel.getBoundingClientRect();
  panel.style.display = pd;
  panel.style.visibility = pv;
  return {w:r.width,h:r.height};
}
function positionPanel(){
  if(panel.style.display==="none") return;
  const vw = window.innerWidth, vh = window.innerHeight;
  const btnRect = mainBtn.getBoundingClientRect();
  const r = panel.getBoundingClientRect();
  const sz = (r.width && r.height) ? {w:r.width, h:r.height} : measurePanelSize();
  let left, top;
  const rightSpace = vw - (btnRect.right + MARGIN);
  const leftSpace = btnRect.left - MARGIN;
  if(rightSpace >= sz.w + 20) {
    left = btnRect.right + MARGIN;
  } else if(leftSpace >= sz.w + 20) {
    left = btnRect.left - sz.w - MARGIN;
  } else {
    left = Math.min(Math.max(btnRect.left + (btnRect.width - sz.w) / 2, MARGIN), vw - sz.w - MARGIN);
  }
  top = btnRect.bottom + MARGIN;
  if(top + sz.h > vh - MARGIN) top = vh - sz.h - MARGIN;
  if(top < MARGIN) top = MARGIN;
  panel.style.left = Math.round(left) + "px";
  panel.style.top = Math.round(top) + "px";
}
function togglePanel(){
  if(panel.style.display === "block"){
    panel.style.display = "none";
  }else{
    panel.style.display = "block";
    positionPanel();
  }
}
document.body.append(mainBtn,panel);
let initLeft = parseFloat(localStorage.getItem("wenku8_btn_left"));
let initTop = parseFloat(localStorage.getItem("wenku8_btn_top"));
if(!Number.isFinite(initLeft)) initLeft = window.innerWidth - (mainBtn.offsetWidth||32) - 12;
if(!Number.isFinite(initTop)) initTop = 12;
setBtnPosition(initLeft,initTop);
clampBtnInView();
let dragging = false, startX = 0, startY = 0, originL = 0, originT = 0;
mainBtn.addEventListener("pointerdown",(e)=>{
  e.preventDefault();
  mainBtn.setPointerCapture(e.pointerId);
  dragging = false;
  const rect = mainBtn.getBoundingClientRect();
  startX = e.clientX;
  startY = e.clientY;
  originL = rect.left;
  originT = rect.top;
  const onMove = (ev)=>{
    const dx = ev.clientX - startX;
    const dy = ev.clientY - startY;
    if(!dragging && (Math.abs(dx)>THRESHOLD || Math.abs(dy)>THRESHOLD)) dragging = true;
    if(dragging){
      let nl = originL + dx, nt = originT + dy;
      const vw = window.innerWidth, vh = window.innerHeight;
      const bw = mainBtn.offsetWidth, bh = mainBtn.offsetHeight;
      if(nl < MARGIN) nl = MARGIN;
      if(nt < MARGIN) nt = MARGIN;
      if(nl + bw > vw - MARGIN) nl = vw - bw - MARGIN;
      if(nt + bh > vh - MARGIN) nt = vh - bh - MARGIN;
      setBtnPosition(nl,nt);
      if(panel.style.display!=="none") positionPanel();
    }
  };
  const onUp = ()=>{
    mainBtn.releasePointerCapture(e.pointerId);
    document.removeEventListener("pointermove",onMove);
    document.removeEventListener("pointerup",onUp);
    clampBtnInView();
    if(panel.style.display!=="none") positionPanel();
    const l = parseFloat(mainBtn.style.left)||0;
    const t = parseFloat(mainBtn.style.top)||0;
    localStorage.setItem("wenku8_btn_left",String(l));
    localStorage.setItem("wenku8_btn_top",String(t));
    if(!dragging) togglePanel();
    dragging = false;
  };
  document.addEventListener("pointermove",onMove);
  document.addEventListener("pointerup",onUp);
});
mainBtn.addEventListener("click",(e)=>{
  e.preventDefault();
  e.stopPropagation();
});
document.addEventListener("pointerdown",(e)=>{
  if(!panel.contains(e.target) && e.target!==mainBtn) panel.style.display="none";
});
window.addEventListener("resize",()=>{
  clampBtnInView();
  if(panel.style.display!=="none") positionPanel();
});
})();