// Variables used by Scriptable. // These must be at the very top of the file. Do not edit. // icon-color: red; icon-glyph: user-astronaut; /** * Author:LSP * Date:2022-12-22 */ // ------------------------------------------------------- // 是否是开发环境,配合手机端调试使用,正式发布设置为false const isDev = false; console.log(`开发环境 👉👉👉👉👉 ${isDev ? 'DEV' : 'RELEASE'}`); console.log(`----------------------------------------`); const remoteRoot = 'https://gitcode.net/enoyee/scriptable/-/raw/master/'; // 依赖包目录 const fm = FileManager.local(); const rootDir = fm.documentsDirectory(); const cacheDir = fm.joinPath(rootDir, 'LSP'); const dependencyFileName = isDev ? "_LSP.js" : `${cacheDir}/_LSP.js`; // 下载依赖包 await downloadLSPDependency(); // ------------------------------------------------------- if (typeof require === 'undefined') require = importModule // 引入相关方法 const { BaseWidget } = require(dependencyFileName); // @定义小组件 class Widget extends BaseWidget { defaultPreference = { version: 20221215, domain: 'https://tophub.today', hotban: [{ title: '微博 · 热搜榜', link: 'https://tophub.today/n/KqndgxeLl9' }], weiboOpenOptions: [ { name: '国内版' }, { name: '国际版' }, { name: '浏览器' }, ], // -------------------------- titleFontSize: 16, titleFontColor: '#FFFFFF', contentFontSize: 13, contentFontColor: '#FFFFFF', contentLineSpacing: 6, // -------------------------- blur: true, bgUrl: 'https://gitcode.net/enoyee/scriptable/-/raw/master/img/bg_1.jpg', // -------------------------- normalItemCount: 5, largeItemCount: 13 }; getCookie() { const { cookie } = this.readWidgetSetting(); this.logDivider(); console.log(`TopHub的cookie-->${cookie}`); this.logDivider(); return cookie; } getHotban = (defaultValue) => JSON.parse(this.useFileManager().readStringCache('hotban') ?? (defaultValue ? defaultValue : JSON.stringify(this.defaultPreference.hotban))); getWeiboOpenType = () => this.getSettingValueByKey('weiboOpenType', this.defaultPreference.weiboOpenOptions[2].name); getTitleFontSize = () => Number(this.getSettingValueByKey('titleFontSize', `${this.defaultPreference.titleFontSize}`)); getTitleFontColor = () => this.getSettingValueByKey('titleFontColor', `${this.defaultPreference.titleFontColor}`); getContentFontSize = () => Number(this.getSettingValueByKey('contentFontSize', `${this.defaultPreference.contentFontSize}`)); getContentFontColor = () => this.getSettingValueByKey('contentFontColor', `${this.defaultPreference.contentFontColor}`); getContentLineSpacing = () => Number(this.getSettingValueByKey('contentLineSpacing', `${this.defaultPreference.contentLineSpacing}`)); getNormalItemCount = () => Number(this.getSettingValueByKey('normalItemCount', `${this.defaultPreference.normalItemCount}`)); getLargeItemCount = () => Number(this.getSettingValueByKey('largeItemCount', `${this.defaultPreference.largeItemCount}`)); getCKDesc = () => { let hasCK = this.getCookie()?.length > 0; return hasCK ? '已登录' : '未登录' }; constructor(scriptName) { super(scriptName); this.changeBgMode2OnLineBg( this.defaultPreference.bgUrl, { blur: this.defaultPreference.blur, blurRadius: 10 } ); } async renderSearchResultView(response, onItemClick) { const { coverArr = [], linkArr = [], titleArr = [], tipArr = [] } = response; // ========================================================= const style = ` :root { --color-primary: #007aff; --divider-color: rgba(60,60,67,0.16); --card-background: #fff; --card-radius: 8px; --list-header-color: rgba(60,60,67,0.6); } body { margin: 10px 0; -webkit-font-smoothing: antialiased; font-family: "SF Pro Display","SF Pro Icons","Helvetica Neue","Helvetica","Arial",sans-serif; accent-color: var(--color-primary); background: #f6f6f6; } .list { margin: 15px; } .list__body { margin-top: 10px; background: var(--card-background); border-radius: var(--card-radius); overflow: hidden; } .form-label { display: flex; align-items: center; } .form-item { display: flex; align-items: center; justify-content: space-between; min-height: 4em; padding: 2px 16px; position: relative; } .form-item + .form-item::before { content: ""; position: absolute; top: 0; left: 0; right: 0; border-top: 0.8px solid var(--divider-color); } .form-item-cover { width: 44px; height: 44px; border-radius: 6px; border: 0; } .form-item-tite { margin: 0px 12px; font-size: 14px; font-weight: 700; } .form-item-desc { color: #999; margin: 0px 12px; font-size: 13px; } @media (prefers-color-scheme: dark) { :root { --divider-color: rgba(84,84,88,0.65); --card-background: #1c1c1e; --list-header-color: rgba(235,235,245,0.6); } body { background: #000; color: #fff; } } `; // ========================================================= const js = ` (() => { const coverArr = JSON.parse('${JSON.stringify(coverArr)}') const titleArr = JSON.parse('${JSON.stringify(titleArr)}') const tipArr = JSON.parse('${JSON.stringify(tipArr)}') const linkArr = JSON.parse('${JSON.stringify(linkArr)}') window.invoke = (title, link) => { window.dispatchEvent( new CustomEvent( 'JBridge', { detail: { title, link } } ) ) } const fragment = document.createDocumentFragment() coverArr.forEach((cover, index) => { const title = titleArr[index]; const tips = tipArr[index]; const link = linkArr[index]; const label = document.createElement("label"); label.className = "form-item"; const divLabel = document.createElement("div"); divLabel.className = 'form-label'; label.appendChild(divLabel); const img = document.createElement("img"); img.src = cover; img.className = 'form-item-cover'; divLabel.appendChild(img); const divContent = document.createElement("div"); divLabel.appendChild(divContent); const divTitle = document.createElement("div"); divTitle.className = 'form-item-tite'; divTitle.innerText = title; divContent.appendChild(divTitle); const divDesc = document.createElement("div"); divDesc.className = 'form-item-desc'; divDesc.innerText = tips; divContent.appendChild(divDesc); const icon = document.createElement('i') icon.className = 'iconfont icon-xuqiudingyue' label.appendChild(icon) label.addEventListener('click', (e) => invoke(title, link)) fragment.appendChild(label); }); document.getElementById('form').appendChild(fragment) })()`; // ========================================================= const html = `
`; // 预览web const previewWebView = new WebView(); await previewWebView.loadHTML(html, 'https://tophub.today'); const injectListener = async () => { const event = await previewWebView.evaluateJavaScript( `(() => { try { const controller = new AbortController() const listener = (e) => { completion(e.detail) controller.abort() } window.addEventListener( 'JBridge', listener, { signal: controller.signal } ) } catch (e) { alert("搜索界面出错:" + e); throw new Error("搜索界面处理出错: " + e); return; } })()`, true).catch((err) => { console.error(err); this.ERRS.push(err); if (!config.runsInApp) { this.notify('APP主界面', `🚫 ${err}`); } else { throw err } }); //////////////////////////////////// onItemClick?.(event); injectListener(); } injectListener().catch((e) => { console.error(e); }); await previewWebView.present(); } async getAppViewOptions() { return { widgetProvider: { small: true, // 是否提供小号组件 medium: true, // 是否提供中号组件 large: true, // 是否提供大号组件 }, // 预览界面的组件设置item settingItems: [ { name: 'hotbanCK', label: '登录TopHub', type: 'cell', icon: 'https://file.ipadown.com/tophub/assets/images/logo.png', needLoading: true, desc: this.getCKDesc() }, { name: 'hotban', label: '热搜榜设置', type: 'cell', icon: { name: 'flame', color: '#EB3323', }, needLoading: true, showDesc: false, }, { name: 'weiboOpenType', label: '微博打开方式', type: 'cell', icon: 'https://gitcode.net/enoyee/scriptable/-/raw/master/img/ic_weibo.png', needLoading: false, default: this.getWeiboOpenType(), }, { name: 'otherSetting', label: '其他设置', type: 'cell', icon: 'https://gitcode.net/4qiao/framework/raw/master/img/icon/setting.gif', needLoading: true, childItems: [ { name: 'titleFontSize', label: '标题文字大小', type: 'cell', icon: { name: 'pencil.and.outline', color: '#ff758f', }, needLoading: false, alert: { title: '标题文字大小', options: [ { key: 'titleFontSize', hint: '请输入字号', } ] }, default: `${this.getTitleFontSize()}`, }, { name: 'titleFontColor', label: '标题文字颜色', type: 'color', icon: { name: 'pencil.and.outline', color: '#c9184a', }, needLoading: false, default: this.getTitleFontColor(), }, { name: 'contentFontSize', label: '榜单文字大小', type: 'cell', icon: { name: 'scribble', color: '#9d4edd', }, needLoading: false, alert: { title: '榜单文字大小', options: [ { key: 'contentFontSize', hint: '请输入字号', } ] }, default: `${this.getContentFontSize()}`, }, { name: 'contentFontColor', label: '榜单文字颜色', type: 'color', icon: { name: 'scribble', color: '#7b2cbf', }, needLoading: false, default: this.getContentFontColor(), }, { name: 'contentLineSpacing', label: '榜单行间距', type: 'cell', icon: { name: 'doc.richtext', color: '#f6aa1c', }, needLoading: false, alert: { title: '榜单行间距', options: [ { key: 'contentLineSpacing', hint: '请输入间距数字', } ] }, default: `${this.getContentLineSpacing()}`, }, { name: 'banCount', label: '榜单条数', type: 'cell', icon: { name: 'doc.text.below.ecg', color: '#00a8e8' }, alert: { title: '榜单显示条数', message: "小组件每次展示的热榜条数", options: [ { key: 'normalItemCount', hint: '请输入中/小号组件热榜条数', }, { key: 'largeItemCount', hint: '请输入大号组件热榜条数', } ] }, needLoading: false, }, ] }, ], // cell类型的item点击回调 onItemClick: async (item) => { let widgetSetting = this.readWidgetSetting(); let insertDesc; switch (item.name) { case 'hotbanCK': const url = "https://tophub.today/login"; const webview = new WebView(); await webview.loadURL(url); await webview.present(); const cookie = await webview.evaluateJavaScript("document.cookie"); console.log(`登录成功,获取到的ck:${cookie}`); widgetSetting.cookie = cookie; insertDesc = cookie?.length > 0 ? '已登录' : '未登录'; break; case 'hotban': const hotSelectIndex = await this.presentSheet({ title: '热榜设置', message: '⊱配置热榜显示榜单内容⊰', options: [{ name: '查看已添加榜单' }, { name: '搜索添加榜单' }, { name: '重置榜单' }], }); if (hotSelectIndex == 0) { const hotbanArr = this.getHotban('[]'); const hotbanTitleArr = hotbanArr.map(hotban => hotban.title.replaceAll(" ", "")); await this.generateAlert('已添加榜单', `${hotbanTitleArr.length > 0 ? hotbanTitleArr.join('、') : '暂无添加,默认微博热搜'}`, ['确定']); } else if (hotSelectIndex == 1) { await this.generateInputAlert({ title: '热榜搜索', options: [{ hint: '请输入关键字', value: '' }] }, async (inputArr) => { const keyword = inputArr[0].value; let response = undefined; try { ////// const webview = new WebView(); await webview.loadURL(`${this.defaultPreference.domain}/search?q=${encodeURIComponent(keyword)}`); const html = await webview.getHTML(); await webview.loadHTML(html); // 通过dom操作把HTML里面的热榜内容提取出来 const getData = ` function getData() { // 图片封面 coverArr = [] // 链接 linkArr = [] // 标题 titleArr = [] // 描述 tipArr = [] nodeSize = 0 totalCount = 20 // 图片封面 let allItemNodeList = document.getElementsByClassName('weui-media-box__thumb radius'); for(let node of allItemNodeList) { if(nodeSize < totalCount) { coverArr.push(node.src) } else { break } nodeSize += 1 } // 链接 nodeSize = 0 allItemNodeList = document.getElementsByClassName('weui-media-box weui-media-box_appmsg weui-cell'); for(let node of allItemNodeList) { if(nodeSize < totalCount) { linkArr.push(node.href) } else { break } nodeSize += 1 } // 标题 nodeSize = 0 allItemNodeList = document.getElementsByClassName('weui-media-box__title'); for(let node of allItemNodeList) { if(nodeSize < totalCount) { titleArr.push(node.innerText) } else { break } nodeSize += 1 } // 订阅人数 nodeSize = 0 allItemNodeList = document.getElementsByClassName('weui-media-box__desc'); for(let node of allItemNodeList) { if(nodeSize < totalCount) { tipArr.push(node.innerText) } else { break } nodeSize += 1 } return { coverArr, linkArr, titleArr, tipArr }; } getData() ` // 热榜数据 response = await webview.evaluateJavaScript(getData, false); const { linkArr = [] } = response; if (linkArr.length === 0) { await this.generateAlert('热榜搜索', '搜索结果为空', ['确定']); } else { await this.renderSearchResultView(response, async (event) => { let { title, link } = event; const hotbanArr = this.getHotban('[]'); const findItem = hotbanArr.find(hotban => hotban.title == title); if (findItem == undefined) { try { link = `${this.defaultPreference.domain}${link}`; hotbanArr.push({ title, link }); this.useFileManager().writeStringCache('hotban', JSON.stringify(hotbanArr)); await this.generateAlert(`热榜内容`, `已成功添加«${title}»\n返回上级点击组件预览即可查看效果`, ['确定']); } catch (error) { console.error(error); } } }); } ////// } catch (error) { console.error(`🚫 热榜搜索出错==>${error}`); await this.generateAlert('🚫 热榜搜索出错', `${error}`, ['确定']); }; }); } else if (hotSelectIndex != -1) { this.useFileManager().writeStringCache('hotban', JSON.stringify([])); await this.generateAlert('热榜设置', '热榜已重置成功', ['确定']); } break; case 'weiboOpenType': const index = await this.presentSheet({ title: '微博打开方式', message: '⊱配置点击微博跳转方式⊰', options: this.defaultPreference.weiboOpenOptions, }); insertDesc = this.defaultPreference.weiboOpenOptions[index].name; widgetSetting.weiboOpenType = insertDesc; break } // 写入更新配置 this.writeWidgetSetting(widgetSetting); return { desc: { value: insertDesc }, }; }, }; } async render({ widgetSetting, family }) { let widget; switch (family) { case 'small': widget = await this.provideSmallWidget(); break; case 'medium': widget = await this.provideMediumWidget(); break; case 'large': widget = await this.provideLargeWidget(); break; } widget.setPadding(6, 16, 6, 12); return widget; } /** * 小型组件 * @returns */ async provideSmallWidget() { return await this.provideWidget(this.getNormalItemCount(), true); } /** * 中型组件 * @returns */ async provideMediumWidget() { return await this.provideWidget(this.getNormalItemCount()); } /** * 大型组件 * @returns */ async provideLargeWidget() { return await this.provideWidget(this.getLargeItemCount()); } async provideWidget(itemCount, small = false) { const defaultHotArr = this.defaultPreference.hotban; const cacheHotStr = this.useFileManager().readStringCache('hotban'); let hotbanArr = cacheHotStr ? JSON.parse(cacheHotStr) : defaultHotArr; hotbanArr = hotbanArr.length === 0 ? defaultHotArr : hotbanArr; const index = this.carouselIndex(`HotbanRandom${this.defaultPreference.version}`, hotbanArr.length); const response = await this.loadHotBanRES(hotbanArr[index].link); // 数据 const { hotTitle = '', logoUrl = '', linkArr = [], titleArr = [] } = response; //================================= const widget = new ListWidget(); //================================= let stack = widget.addStack(); stack.layoutVertically(); // 标题 let titleStack = stack.addStack(); titleStack.size = new Size(0, 44); titleStack.centerAlignContent(); titleStack.layoutHorizontally(); let img = await this.getImageByUrl(logoUrl); let imgSpan = titleStack.addImage(img); imgSpan.imageSize = new Size(23, 23); imgSpan.cornerRadius = 6; titleStack.addSpacer(8); let textSpan = titleStack.addText(hotTitle.replace('\n', '')); textSpan.textColor = new Color(this.getTitleFontColor()); let titleSize = this.getTitleFontSize(); if (small) { titleSize -= 2; } textSpan.font = Font.semiboldSystemFont(titleSize); textSpan.lineLimit = 1; // titleStack.addSpacer(); if (!small) { img = this.getSFSymbol('goforward'); imgSpan = titleStack.addImage(img); imgSpan.imageSize = new Size(12, 12); imgSpan.tintColor = new Color(this.getTitleFontColor()); titleStack.addSpacer(4); textSpan = titleStack.addText(this.getDateStr(new Date(), 'HH:mm')); textSpan.textColor = new Color(this.getTitleFontColor()); textSpan.font = Font.semiboldSystemFont(titleSize - 4); titleStack.addSpacer(10); } // item for (let index = 0; index < itemCount; index++) { const name = titleArr[index]; const link = linkArr[index]; let nameStack = stack.addStack(); stack.addSpacer(this.getContentLineSpacing()); nameStack.layoutHorizontally(); textSpan = nameStack.addText(`${index + 1}. `); let hotTop = index <= 2; textSpan.textColor = new Color(`${hotTop ? '#ef233c' : this.getContentFontColor()}`); let contentSize = this.getContentFontSize(); textSpan.font = hotTop ? Font.italicSystemFont(contentSize) : Font.systemFont(contentSize); // textSpan = nameStack.addText(`${name}`); textSpan.textColor = new Color(this.getContentFontColor()); textSpan.font = Font.systemFont(contentSize); textSpan.lineLimit = 1; // let linkElement = link; if (!link?.startsWith('http')) { linkElement = `https://tophub.today${link}`; } if (hotTitle.search("微博") != -1) { const weiboOpenType = this.getWeiboOpenType(); if (weiboOpenType == '国内版') { // 微博客户端 linkElement = 'sinaweibo://searchall?q=' + encodeURIComponent(`#${name}#`) } else if (weiboOpenType == '国际版') { // 微博国际版客户端 linkElement = 'weibointernational://search?q=' + encodeURIComponent(`#${name}#`) } } nameStack.url = linkElement; } stack.addSpacer(); //================================= return widget; } // --------------------------NET START-------------------------- async loadHTML(url) { let req = new Request(url); req.headers = { "cookie": this.getCookie(), "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36", }; let html = await req.loadString(); return html.replace(/(\r\n|\n|\r)/gm, ""); } async loadHotBanRES(link) { // 热榜数据 let response = undefined; try { const ufm = this.useFileManager(); const webview = new WebView(); const cacheFileName = this.md5(link); const lastCacheTime = ufm.getCacheModifyDate(cacheFileName); const timeInterval = Math.floor((this.getCurrentTimeStamp() - lastCacheTime) / 60); // 读取本地缓存 const localCache = ufm.readStringCache(cacheFileName); const canUseCache = localCache != null && localCache.length > 0; // 过时且有本地缓存则直接返回本地缓存数据 const { refreshInterval = '0' } = this.readWidgetSetting(); const shouldLoadCache = timeInterval <= Number(refreshInterval) && canUseCache; let html = ufm.readStringCache(cacheFileName); if (!shouldLoadCache) { console.log(`-->>在线加载网页数据:${link}`); html = await this.loadHTML(link); ufm.writeStringCache(cacheFileName, html); } else { console.log(`-->>加载缓存网页数据:${link}`); } await webview.loadHTML(html); // 通过dom操作把HTML里面的热榜内容提取出来 const getData = ` function getData() { // logo链接 logoUrl = '' // 榜单标题 hotTitle = '--' branLogoArr = document.getElementsByClassName('brand logo') if(JSON.stringify(branLogoArr) == '{}') { return getPCData() } if(branLogoArr.length > 0) { branLogo = branLogoArr[0] logoUrl = branLogo.style['background-image'].slice(5).slice(0, -2) // mainTitle = branLogo.innerText subTitle = document.getElementsByClassName('tab-nav-item active')[0]?.innerText ?? '' hotTitle = mainTitle + (subTitle.length > 0 ? ' · ' : '') + subTitle } // 链接 linkArr = [] // 标题 titleArr = [] allItemNodeList = document.querySelectorAll('.rank-item-container') // 链接&标题 nodeSize = 0 for(let node of allItemNodeList) { if(nodeSize < 30) { link = node.href; linkArr.push(link); title = node.getElementsByClassName('s-title')[0].innerText titleArr.push(title); } else { break } nodeSize += 1 } return { hotTitle, logoUrl, linkArr, titleArr }; } function getPCData() { // 获取榜单标题 hotTitle = document.querySelector('.Xc-ec-L').innerText // 获取logo链接 logoImgHtml = document.querySelector('#tabbed-header-panel div').innerHTML; // 提取src的正则表达式 logoPattern = /]*href=['"]([^'"]+)[^>]*/gi, function(match, link){ linkArr.push(link) }); // 标题 titleArr.push(node.innerText) } return { hotTitle, logoUrl, linkArr, titleArr }; } getData() ` // 热榜数据 response = await webview.evaluateJavaScript(getData, false); if (response.titleArr?.length > 0) { this.useFileManager().writeStringCache('hot', JSON.stringify(response)); } } catch (error) { console.error(`🚫 请求热板数据出错了=>${error}`); response = JSON.parse(this.useFileManager().readStringCache('hot')); } return response; } // --------------------------NET END-------------------------- } await new Widget(Script.name()).run(); // ================================================================================= // ================================================================================= async function downloadLSPDependency() { let fm = FileManager.local(); const dependencyURL = `${remoteRoot}_LSP.js`; if (isDev) { const iCloudPath = FileManager.iCloud().documentsDirectory(); const localIcloudDependencyExit = fm.isFileStoredIniCloud(`${iCloudPath}/_LSP.js`); const localDependencyExit = fm.fileExists(`${rootDir}/_LSP.js`); const fileExist = localIcloudDependencyExit || localDependencyExit; console.log(`🚀 DEV开发依赖文件${fileExist ? '已存在 ✅' : '不存在 🚫'}`); if (!fileExist) { console.log(`🤖 DEV 开始下载依赖~`); await downloadFile2Scriptable('_LSP', dependencyURL); } return } ////////////////////////////////////////////////////////// console.log(`----------------------------------------`); const remoteDependencyExit = fm.fileExists(`${cacheDir}/_LSP.js`); console.log(`🚀 RELEASE依赖文件${remoteDependencyExit ? '已存在 ✅' : '不存在 🚫'}`); console.log(`----------------------------------------`); // ------------------------------ if (!remoteDependencyExit) { // 下载依赖 // 创建根目录 if (!fm.fileExists(cacheDir)) { fm.createDirectory(cacheDir, true); } // 下载 console.log('🤖 RELEASE开始下载依赖~'); console.log(`----------------------------------------`); const req = new Request(dependencyURL); const moduleJs = await req.load(); if (moduleJs) { fm.write(fm.joinPath(cacheDir, '/_LSP.js'), moduleJs); console.log('✅ LSP远程依赖环境下载成功!'); console.log(`----------------------------------------`); } else { console.error('🚫 获取依赖环境脚本失败,请重试!'); console.log(`----------------------------------------`); } } } /** * 获取保存的文件名 * @param {*} fileName * @returns */ function getSaveFileName(fileName) { const hasSuffix = fileName.lastIndexOf(".") + 1; return !hasSuffix ? `${fileName}.js` : fileName; }; /** * 保存文件到Scriptable软件目录,app可看到 * @param {*} fileName * @param {*} content * @returns */ function saveFile2Scriptable(fileName, content) { try { const fm = FileManager.iCloud(); let fileSimpleName = getSaveFileName(fileName); const filePath = fm.joinPath(fm.documentsDirectory(), fileSimpleName); fm.writeString(filePath, content); return true; } catch (error) { return false; } }; /** * 下载js文件到Scriptable软件目录 * @param {*} moduleName 名称 * @param {*} url 在线地址 * @returns */ async function downloadFile2Scriptable(moduleName, url) { const req = new Request(url); const content = await req.loadString(); return saveFile2Scriptable(`${moduleName}`, content); }; // ================================================================================= // =================================================================================