// ==UserScript==
// @name 移动端网页字体修改器
// @namespace http://via-browser.com/
// @version 2.5.1
// @description 支持字体和颜色管理的移动端字体工具
// @author ^o^
// @match *://*/*
// @run-at document-start
// @grant GM_registerMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
const C={DB_NAME:'VIA_FONT_DB',FONT_TYPES:{ttf:{format:'truetype'},otf:{format:'opentype'},woff:{format:'woff'},woff2:{format:'woff2'}},DEFAULT_FONT:'system-ui',DEFAULT_COLOR:'#333333'};
class StorageManager{static get(){const e=GM_getValue('fontSettings',{currentFont:C.DEFAULT_FONT,fabPosition:null,fontColor:C.DEFAULT_COLOR});return{currentFont:e.currentFont||C.DEFAULT_FONT,fabPosition:e.fabPosition||null,fontColor:e.fontColor||C.DEFAULT_COLOR}}static save(e){GM_setValue('fontSettings',{currentFont:e.currentFont,fabPosition:e.fabPosition,fontColor:e.fontColor})}}class DatabaseManager{constructor(){this.db=null,this.dbName=C.DB_NAME,this.storeName='fontStore'}init(){return new Promise((e,t)=>{const n=indexedDB.open(this.dbName,1);n.onupgradeneeded=n=>{const t=n.target.result;t.objectStoreNames.contains(this.storeName)||t.createObjectStore(this.storeName,{keyPath:'id'})},n.onsuccess=n=>{this.db=n.target.result,e()},n.onerror=n=>{t(`IndexedDB打开失败: ${n.target.error}`)}})}getFonts(){return new Promise((e,t)=>{const n=this.db.transaction(this.storeName,'readonly'),o=n.objectStore(this.storeName),i=o.getAll();i.onsuccess=()=>{e(i.result.filter(e=>'settings'!==e.id))},i.onerror=e=>{t(`获取字体失败: ${e.target.error}`)}})}saveFont(e){return new Promise((t,n)=>{const o=this.db.transaction(this.storeName,'readwrite'),i=o.objectStore(this.storeName),r=i.put(e);r.onsuccess=()=>t(),r.onerror=e=>{n(`保存字体失败: ${e.target.error}`)}})}deleteFont(e){return new Promise((t,n)=>{const o=this.db.transaction(this.storeName,'readwrite'),i=o.objectStore(this.storeName),r=i.delete(e);r.onsuccess=()=>t(),r.onerror=e=>{n(`删除字体失败: ${e.target.error}`)}})}}
class UIManager{static createFAB(e){const t=document.createElement('div');t.id='via-font-fab';const n=e.fabPosition?`${e.fabPosition.x}px`:'auto',o=e.fabPosition?`${e.fabPosition.y}px`:'auto',i=e.fabPosition?'auto':'20px',r=e.fabPosition?'auto':'30px';return Object.assign(t.style,{position:'fixed',width:'50px',height:'50px',background:'linear-gradient(45deg, #2196F3, #9C27B0)',color:'white',borderRadius:'50%',textAlign:'center',lineHeight:'50px',fontSize:'24px',fontWeight:'bold',boxShadow:'0 4px 8px rgba(0,0,0,0.3)',zIndex:999999,touchAction:'none',userSelect:'none',left:n,top:o,right:i,bottom:r,transition:'left 0.2s, top 0.2s, transform 0.2s ease, opacity 0.2s ease',opacity:'0.7',transform:'scale(0.8)'}),t.innerHTML='Aa',document.body.appendChild(t),t}static createPanel(){const e=document.createElement('div');e.id='via-font-panel';const t=document.createElement('div');t.style.display='none';return Object.assign(e.style,{position:'fixed',bottom:'0',left:'0',right:'0',background:'rgba(255,255,255,0.95)',backdropFilter:'blur(15px)',borderRadius:'16px 16px 0 0',boxShadow:'0 -8px 20px rgba(0,0,0,0.15)',padding:'20px',transform:'translateY(100%)',transition:'transform 0.3s ease',maxHeight:'80vh',overflowY:'auto',zIndex:999998}),e.innerHTML=`<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;padding-bottom:15px;border-bottom:1px solid #eee"><h3 style="margin:0;font-weight:600;color:#333">字体设置</h3><button class="close-btn" style="background:none;border:none;font-size:22px;cursor:pointer;color:#999">×</button></div><div class="content"></div>`,document.body.appendChild(e),e}}
let fm=null,isFabV=GM_getValue('fabVisible',!0);function openFontSettings(){fm&&fm.togglePanel()}function toggleFab(){fm&&(isFabV=!isFabV,isFabV?(fm.fab.style.display="block",GM_setValue('fabVisible',!0)):(fm.fab.style.display="none",GM_setValue('fabVisible',!1)))}GM_registerMenuCommand('打开字体设置',openFontSettings),GM_registerMenuCommand('切换悬浮球显示',toggleFab);
class FontManager{constructor(){this.db=new DatabaseManager,this.state=StorageManager.get(),this.state.localFonts=[],this.dragging=!1,this.fabTimer=null,this.edgeTimer=null;'loading'===document.readyState?document.addEventListener('DOMContentLoaded',()=>this.init()):this.init()}async init(){try{await this.db.init(),this.state.localFonts=await this.db.getFonts(),this.applySettings(),this.createUI(),this.setupDrag(),this.setupEdge()}catch(e){console.error('字体管理器初始化失败:',e)}}createUI(){this.fab=UIManager.createFAB(this.state),this.panel=UIManager.createPanel(),this.fab.addEventListener('click',e=>{this.dragging||this.togglePanel()}),isFabV||(this.fab.style.display="none")}setupEdge(){this.checkEdge(),window.addEventListener('resize',()=>this.checkEdge())}checkEdge(){if(this.fab){clearTimeout(this.edgeTimer);const e=this.fab.getBoundingClientRect(),t=window.innerWidth,n=10;e.left<n?(this.fab.style.transform='scale(0.8) translateX(-40%)',this.fab.style.opacity='0.5'):t-e.right<n?(this.fab.style.transform='scale(0.8) translateX(40%)',this.fab.style.opacity='0.5'):(this.fab.style.transform='scale(0.8)',this.fab.style.opacity='0.7')}}setupDrag(){let sX,sY,initX,initY;const i=e=>{if(e.touches[0]){const t=e.touches[0];sX=t.clientX,sY=t.clientY,initX=this.fab.offsetLeft,initY=this.fab.offsetTop,this.fab.style.transition='none',this.fab.style.opacity='1',this.fab.style.transform='scale(1)',clearTimeout(this.fabTimer),clearTimeout(this.edgeTimer),document.addEventListener('touchmove',r),document.addEventListener('touchend',s)}},r=e=>{if(e.touches[0]){const t=e.touches[0],n=t.clientX-sX,o=t.clientY-sY;(Math.abs(n)>5||Math.abs(o)>5)&&(this.dragging=!0);let i=initX+n,a=initY+o,c=window.innerWidth-this.fab.offsetWidth,u=window.innerHeight-this.fab.offsetHeight;i=Math.max(0,Math.min(i,c)),a=Math.max(0,Math.min(a,u)),this.fab.style.left=`${i}px`,this.fab.style.top=`${a}px`,this.fab.style.right='auto',this.fab.style.bottom='auto'}},s=()=>{document.removeEventListener('touchmove',r),document.removeEventListener('touchend',s),this.fab.style.transition='left 0.2s, top 0.2s, transform 0.2s ease, opacity 0.2s ease',this.dragging&&(this.state.fabPosition={x:this.fab.offsetLeft,y:this.fab.offsetTop},StorageManager.save(this.state)),this.dragging=!1,clearTimeout(this.fabTimer),this.fabTimer=setTimeout(()=>{this.fab.style.opacity='0.7',this.fab.style.transform='scale(0.8)',this.edgeTimer=setTimeout(()=>this.checkEdge(),100)},800)};this.fab.addEventListener('touchstart',i)}togglePanel(){'none'===this.panel.style.display?(this.panel.style.display='block',setTimeout(()=>{this.panel.style.transform='translateY(0)'},10),this.refreshPanel()):(this.panel.style.transform='translateY(100%)',setTimeout(()=>{this.panel.style.display='none'},300))}async refreshPanel(){const e=this.panel.querySelector('.content');e.innerHTML=`<div style="margin-bottom:20px"><label style="display:block;margin-bottom:8px;font-weight:500">当前字体</label><select class="font-select" style="width:100%;padding:12px;border-radius:8px;border:1px solid #ddd;font-size:16px"><option value="system-ui">系统默认</option>${this.state.localFonts.map(e=>`<option value="${e.name}" ${this.state.currentFont===e.name?'selected':''}>${e.name}</option>`).join('')}</select></div><div style="margin-bottom:25px"><label style="display:block;margin-bottom:8px;font-weight:500">字体颜色</label><div style="display:flex;align-items:center;gap:10px"><input type="color" class="color-picker" value="${this.state.fontColor}" style="width:50px;height:40px;padding:2px;border-radius:6px;border:1px solid #ddd"><input type="text" class="color-input" value="${this.state.fontColor}" style="flex:1;padding:10px;border-radius:8px;border:1px solid #ddd;font-size:14px"><button class="color-reset-btn" style="padding:8px 15px;background:#f0f0f0;border:1px solid #ddd;border-radius:6px;cursor:pointer">重置</button></div></div><div style="margin:25px 0;padding:20px 15px;background:#f9f9f9;border-radius:10px;border:2px dashed #ddd;text-align:center"><label style="display:block;margin-bottom:15px;font-size:16px;color:#555">上传字体文件 (${Object.keys(C.FONT_TYPES).join(', ')})</label><input type="file" accept="${Object.keys(C.FONT_TYPES).map(e=>`.${e}`).join(',')}" multiple style="width:100%;padding:10px"></div><div style="margin-top:25px"><h4 style="margin-bottom:15px;font-size:17px">已安装字体 (${this.state.localFonts.length})</h4><div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(160px, 1fr));gap:15px">${this.state.localFonts.map(e=>`<div style="background:#f5f5f5;padding:15px;border-radius:10px;text-align:center"><div style="font-size:15px;margin-bottom:12px">${e.name}</div><button data-font="${e.name}" class="delete-btn" style="padding:8px 15px;background:#ff4d4f;color:white;border:none;border-radius:6px;cursor:pointer;width:100%">删除</button></div>`).join('')}</div></div>`,this.setupEvents()}setupEvents(){this.panel.querySelector('.font-select').addEventListener('change',e=>{this.applyFont(e.target.value)});const e=this.panel.querySelector('.color-picker'),t=this.panel.querySelector('.color-input'),n=this.panel.querySelector('.color-reset-btn');e.addEventListener('input',e=>{t.value=e.target.value,this.applyColor(e.target.value)}),t.addEventListener('input',e=>{const n=e.target.value;/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(n)&&(e.value=n,this.applyColor(n))}),n.addEventListener('click',()=>{e.value=C.DEFAULT_COLOR,t.value=C.DEFAULT_COLOR,this.applyColor(C.DEFAULT_COLOR)}),this.panel.querySelector('input[type="file"]').addEventListener('change',async e=>{await this.handleFiles(Array.from(e.target.files)),this.refreshPanel()}),this.panel.querySelectorAll('.delete-btn').forEach(e=>{e.addEventListener('click',()=>{this.delFont(e.dataset.font)})}),this.panel.querySelector('.close-btn').addEventListener('click',()=>{this.togglePanel()})}async handleFiles(e){for(const t of e)await this.processFile(t)}async processFile(e){const t=e.name.split('.').pop().toLowerCase();if(!C.FONT_TYPES[t])return void alert(`不支持的文件类型: .${t}`);let n=prompt('字体名称:',e.name.replace(/\.[^.]+$/,''));if(!n)return;const o=this.state.localFonts.some(e=>e.name===n);if(o&&!confirm(`字体"${n}"已存在,是否覆盖?`))return;const i=await this.readFile(e),r={id:n,name:n,data:i,format:C.FONT_TYPES[t].format};try{await this.db.saveFont(r),this.state.localFonts=await this.db.getFonts(),(this.state.currentFont===n||!this.state.currentFont)&&this.applyFont(n)}catch(e){console.error('保存字体失败:',e),alert('字体保存失败,请查看控制台获取详细信息')}}readFile(e){return new Promise(t=>{const n=new FileReader;n.onload=e=>t(e.target.result),n.readAsDataURL(e)})}async delFont(e){if(!confirm(`确定删除"${e}"?`))return;try{await this.db.deleteFont(e),this.state.localFonts=this.state.localFonts.filter(t=>t.name!==e),this.state.currentFont===e&&this.applyFont(C.DEFAULT_FONT),this.refreshPanel()}catch(e){console.error('删除字体失败:',e),alert('字体删除失败,请查看控制台获取详细信息')}}applyColor(e){const t=document.getElementById('via-font-color-style');t&&t.remove(),e&&e!==C.DEFAULT_COLOR&&((t=document.createElement('style')).id='via-font-color-style',t.textContent=`body, body * { color: ${e} !important; }`,document.head.appendChild(t)),this.state.fontColor=e||C.DEFAULT_COLOR,StorageManager.save(this.state)}async applyFont(e){document.querySelectorAll('style[data-via-font]').forEach(e=>e.remove());if(e!==C.DEFAULT_FONT){const t=this.state.localFonts.find(t=>t.name===e);if(t){const n=document.createElement('style');n.dataset.viaFont=e,n.textContent=`@font-face {font-family:"${e}";src:url("${t.data}") format("${t.format}");font-display:swap;}body, body * {font-family:"${e}", sans-serif !important;}`,document.head.appendChild(n)}else e=C.DEFAULT_FONT}this.state.currentFont=e,StorageManager.save(this.state),this.panel&&this.panel.querySelector('.font-select')&&(this.panel.querySelector('.font-select').value=e)}applySettings(){this.applyFont(this.state.currentFont),this.applyColor(this.state.fontColor)}}document.addEventListener('DOMContentLoaded',()=>{fm||(fm=new FontManager)},{once:!0});