From 876a28904c3d61884ea874d5482211a6b602f6b4 Mon Sep 17 00:00:00 2001 From: aooiuu Date: Wed, 8 May 2024 19:28:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=AB=A0=E8=8A=82?= =?UTF-8?q?=E5=88=87=E6=8D=A2=E8=8F=9C=E5=8D=95=20(#7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 7 +- packages/shared/src/RecordFile.ts | 11 +- packages/shared/src/localBookManager.ts | 29 +- packages/shared/src/ruleFileManager.ts | 11 +- packages/vscode/package.json | 5 +- packages/vscode/src/App.ts | 97 +----- .../treeview/TreeItemDecorationProvider.ts | 4 + packages/vscode/src/treeview/book.ts | 33 +- packages/vscode/src/treeview/bookManager.ts | 7 +- packages/vscode/src/treeview/favorites.ts | 4 + packages/vscode/src/treeview/history.ts | 4 + packages/vscode/src/treeview/localBook.ts | 13 +- packages/vscode/src/treeview/source.ts | 4 + packages/vscode/src/utils/easyPostMessage.ts | 4 + packages/vscode/src/utils/sleep.ts | 8 + packages/vscode/src/webview/index.ts | 323 +++++++++++++++--- packages/web/.eslintrc.cjs | 3 +- packages/web/src/api/index.ts | 21 ++ packages/web/src/pages/content/index.vue | 123 +++++++ packages/web/src/router/index.ts | 5 + packages/web/src/stores/chapters.ts | 28 ++ pnpm-lock.yaml | 40 ++- 22 files changed, 610 insertions(+), 174 deletions(-) create mode 100644 packages/vscode/src/utils/sleep.ts create mode 100644 packages/web/src/pages/content/index.vue create mode 100644 packages/web/src/stores/chapters.ts diff --git a/package.json b/package.json index 5527def0..04ed91ec 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "@any-reader/monorepo", "type": "module", - "version": "0.1.0-alpha.3", + "version": "1.0.0", "packageManager": "pnpm@8.6.5", "description": "", - "license": "MIT", + "license": "GPL3", "author": "aooiuu", "homepage": "https://github.com/aooiuu/any-reader#readme", "repository": { @@ -28,12 +28,13 @@ "build:core": "npm -C packages/core run build", "build:shared": "npm -C packages/shared run build", "run:web": "npm -C packages/web run dev", + "build:web": "npm -C packages/web run build", "build:web-w": "npm -C packages/web run build:w", "run:vsc": "npm-run-all build:core build:web-w", "docs": "npm -C docs run docs:dev", "build:docs": "npm -C docs run docs:build", "server": "npm -C packages/server run dev", - "build": "npm-run-all build:core build:shared", + "build": "npm-run-all build:core build:shared build:web", "run:server": "run-p server run:web", "test": "jest", "coveralls": "jest --coverage", diff --git a/packages/shared/src/RecordFile.ts b/packages/shared/src/RecordFile.ts index 726c3e6a..971048b3 100644 --- a/packages/shared/src/RecordFile.ts +++ b/packages/shared/src/RecordFile.ts @@ -1,4 +1,5 @@ -import * as fs from 'fs-extra' +// @ts-expect-error +import { ensureFile, readJson, writeJson } from 'fs-extra/esm' import type { Rule, SearchItem } from '@any-reader/core' export interface RecordFileRow extends SearchItem { @@ -18,8 +19,8 @@ export class RecordFile { // 初始化 async init() { - await fs.ensureFile(this.filePath) - this.history = await fs.readJson(this.filePath).catch(() => this.history) + await ensureFile(this.filePath) + this.history = await readJson(this.filePath).catch(() => this.history) } // 获取所有记录 @@ -29,8 +30,8 @@ export class RecordFile { // 保存配置文件 async writeFile() { - await fs.ensureFile(this.filePath) - await fs.writeJson(this.filePath, this.history, { spaces: 2 }) + await ensureFile(this.filePath) + await writeJson(this.filePath, this.history, { spaces: 2 }) } // 删除记录 diff --git a/packages/shared/src/localBookManager.ts b/packages/shared/src/localBookManager.ts index 22ee945b..64ad8260 100644 --- a/packages/shared/src/localBookManager.ts +++ b/packages/shared/src/localBookManager.ts @@ -1,11 +1,11 @@ import * as path from 'node:path' -import * as fs from 'fs-extra' +import * as fs from 'node:fs' import EPub from 'epub' import Encoding from 'encoding-japanese' import * as iconv from 'iconv-lite' import { LOCAL_BOOK_DIR } from './constants' -enum BOOK_TYPE { +export enum BOOK_TYPE { TXT = 1, EPUB = 2, } @@ -30,9 +30,17 @@ export function checkDir() { fs.mkdirSync(LOCAL_BOOK_DIR) } +export function path2bookFile(filePath: string): BookFile { + return { + type: getBookType(filePath), + name: path.basename(filePath, path.extname(filePath)), + path: filePath, + } +} + // 获取书籍类型 -function getBookType(extname: string) { - return extname === '.txt' ? BOOK_TYPE.TXT : BOOK_TYPE.EPUB +export function getBookType(filePath: string) { + return path.extname(filePath) === '.txt' ? BOOK_TYPE.TXT : BOOK_TYPE.EPUB } // 获取所有书籍 @@ -43,18 +51,15 @@ export async function getBookList(): Promise { const files = fs.readdirSync(dir) return files - .filter(f => ['.txt', '.epub'].includes(path.extname(f))) - .map((f) => { - return { - type: getBookType(path.extname(f)), - name: path.basename(f, path.extname(f)), - path: path.join(dir, f), - } + .filter(filePath => ['.txt', '.epub'].includes(path.extname(filePath))) + .map((filePath) => { + return path2bookFile(path.join(dir, filePath)) }) } // 获取章节 -export async function getChapter(bookFile: BookFile): Promise { +export async function getChapter(filePath: string): Promise { + const bookFile = path2bookFile(filePath) if (bookFile.type === BOOK_TYPE.TXT) { return [ { diff --git a/packages/shared/src/ruleFileManager.ts b/packages/shared/src/ruleFileManager.ts index 56ddf81f..cfe463d4 100644 --- a/packages/shared/src/ruleFileManager.ts +++ b/packages/shared/src/ruleFileManager.ts @@ -1,4 +1,5 @@ -import * as fs from 'fs-extra' +// @ts-expect-error +import { ensureFile, readJson, writeJson } from 'fs-extra/esm' import { v4 as uuidV4 } from 'uuid' import type { Rule } from '@any-reader/core' import { decodeRule } from '@any-reader/core' @@ -8,7 +9,7 @@ let ruleList: Rule[] = [] async function readRuleList(): Promise { try { - const list = await fs.readJson(BOOK_SOURCE_PATH) + const list = await readJson(BOOK_SOURCE_PATH) for (let i = 0; i < list.length; i++) { const rule = list[i] if (typeof rule === 'string' && rule.includes('eso://')) @@ -23,14 +24,14 @@ async function readRuleList(): Promise { // 初始化 export async function init() { - await fs.ensureFile(BOOK_SOURCE_PATH) + await ensureFile(BOOK_SOURCE_PATH) ruleList = await readRuleList() } // 保存配置文件 async function writeFile() { - await fs.ensureFile(BOOK_SOURCE_PATH) - return fs.writeJson(BOOK_SOURCE_PATH, ruleList, { spaces: 2 }) + await ensureFile(BOOK_SOURCE_PATH) + return writeJson(BOOK_SOURCE_PATH, ruleList, { spaces: 2 }) } export function list(): Rule[] { diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 2d5ff0c2..f5f1a217 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -1,9 +1,9 @@ { "name": "any-reader", "displayName": "any-reader", - "description": "any-reader for vscode", + "description": "自定义规则多站点聚合搜索阅读小说、漫画。包含JS规则解析库和VSCode插件。支持本地小说 TXT、EPUB", "icon": "resources/icon.png", - "version": "0.6.1", + "version": "0.6.2", "preview": true, "publisher": "aooiu", "qna": "https://github.com/aooiuu/any-reader/issues", @@ -262,6 +262,7 @@ "easy-post-message": "^0.1.0", "explorer-opener": "^1.0.1", "fs-extra": "^11.1.1", + "qs": "^6.12.1", "uuid": "^9.0.1" } } diff --git a/packages/vscode/src/App.ts b/packages/vscode/src/App.ts index 731cd6ac..1cceb0b0 100644 --- a/packages/vscode/src/App.ts +++ b/packages/vscode/src/App.ts @@ -1,16 +1,15 @@ import * as vscode from 'vscode'; import { openExplorer } from 'explorer-opener'; -import { ContentType, Rule, RuleManager, SearchItem } from '@any-reader/core'; +import { Rule, RuleManager, SearchItem } from '@any-reader/core'; import { CONSTANTS } from '@any-reader/shared'; -import { BookChapter, getContent, checkDir } from '@any-reader/shared/localBookManager'; +import { checkDir } from '@any-reader/shared/localBookManager'; import { COMMANDS, BOOK_SOURCE_PATH } from './constants'; -import { config } from './config'; import bookProvider from './treeview/book'; import historyProvider from './treeview/history'; import sourceProvider from './treeview/source'; import favoritesProvider from './treeview/favorites'; import localProvider from './treeview/localBook'; -import bookManager, { TreeNode } from './treeview/bookManager'; +import bookManager from './treeview/bookManager'; import { treeItemDecorationProvider } from './treeview/TreeItemDecorationProvider'; import * as ruleFileManager from './utils/ruleFileManager'; import historyManager from './utils/historyManager'; @@ -27,6 +26,7 @@ class App { // 初始化配置文件 await Promise.all([ruleFileManager.init(), historyManager.init(), favoritesManager.init()]); + // 注册命令 const registerCommand = vscode.commands.registerCommand; [ vscode.window.registerFileDecorationProvider(treeItemDecorationProvider), @@ -35,10 +35,10 @@ class App { registerCommand(COMMANDS.getChapter, this.getChapter, this), registerCommand(COMMANDS.discover, this.discover, this), registerCommand(COMMANDS.searchBookByRule, this.searchBookByRule, this), - registerCommand(COMMANDS.getContent, this.getContent, this), + registerCommand(COMMANDS.getContent, this.webView.getContent, this.webView), registerCommand(COMMANDS.openLocalBookDir, this.openLocalBookDir, this), registerCommand(COMMANDS.refreshLocalBooks, this.refreshLocalBooks, this), - registerCommand(COMMANDS.getContentLocalBook, this.getContentLocalBook, this), + registerCommand(COMMANDS.getContentLocalBook, this.webView.getContentLocalBook, this.webView), registerCommand(COMMANDS.star, this.star, this), registerCommand(COMMANDS.unstar, this.unstar, this), registerCommand(COMMANDS.home, () => this.webView.navigateTo('/'), this.webView), @@ -95,7 +95,9 @@ class App { bookProvider.refresh(); } - // 获取章节 + /** + * 获取章节 + */ async getChapter(history: RecordFileRow, config: { saveHistory: SearchItem }) { await vscode.window.withProgress( { @@ -107,13 +109,7 @@ class App { const rule = await ruleFileManager.findById(history.ruleId); const ruleManager = new RuleManager(rule); const chapterItems = await ruleManager.getChapter(history.url); - - bookManager.list = chapterItems.map((chapterItem: any) => ({ - rule, - type: 2, - data: chapterItem - })); - bookProvider.refresh(); + bookProvider.setChapters(chapterItems, rule, history.url); if (config?.saveHistory) { historyManager.add(config.saveHistory, rule); @@ -123,48 +119,6 @@ class App { ); } - // 获取文章详情 - async getContent(article: TreeNode) { - await vscode.window.withProgress( - { - location: vscode.ProgressLocation.Window, - title: 'loading...', - cancellable: false - }, - async () => { - const textArr = await bookManager.getContent(article); - if (!textArr?.length) { - vscode.window.showWarningMessage('empty content'); - } else { - let content = ''; - if (article.rule.contentType === ContentType.VIDEO) { - this.webView.navigateTo('/player?url=' + textArr[0]); - } else if (article.rule.contentType === ContentType.MANGA) { - content = textArr.map((src) => ``).join(''); - } else { - content = textArr.join(''); - } - this.openWebviewPanel(article.data.name, content); - } - } - ); - } - - // 阅读本地书籍 - async getContentLocalBook(item: BookChapter) { - await vscode.window.withProgress( - { - location: vscode.ProgressLocation.Window, - title: 'loading...', - cancellable: false - }, - async () => { - const content = await getContent(item).catch(() => ''); - this.openWebviewPanel(item.name, content); - } - ); - } - // 打开本地书籍目录 openLocalBookDir() { checkDir(); @@ -176,37 +130,6 @@ class App { localProvider.refresh(); } - openWebviewPanel(title: string, content: string) { - if (!content) { - return; - } - if (config.app.get('hideImage', false)) { - content = content.replace(//gim, ''); - } - const injectedHtml = config.app.get('injectedHtml', ''); - const css = ` - html, - body { - margin: 0; - padding: 0; - height: 100%; - width: 100%; - } - body { - font-size: 1em; - } - p { - margin: 0; - padding: 0; - } - `; - this.webView.openWebviewPanel( - title, - `${injectedHtml}
${content}
` - ); - } - // 获取本地书源列表 async getBookSource() { sourceProvider.refresh(); diff --git a/packages/vscode/src/treeview/TreeItemDecorationProvider.ts b/packages/vscode/src/treeview/TreeItemDecorationProvider.ts index e2461389..1a94c0dc 100644 --- a/packages/vscode/src/treeview/TreeItemDecorationProvider.ts +++ b/packages/vscode/src/treeview/TreeItemDecorationProvider.ts @@ -1,3 +1,7 @@ +/** + * [侧边栏 - 规则] 右侧规则类型显示 + */ + import { FileDecoration, FileDecorationProvider, ProviderResult, Uri } from 'vscode'; import { CONTENT_TYPE_TEXT } from '@any-reader/core'; diff --git a/packages/vscode/src/treeview/book.ts b/packages/vscode/src/treeview/book.ts index 2b1ae1e2..4c10c840 100644 --- a/packages/vscode/src/treeview/book.ts +++ b/packages/vscode/src/treeview/book.ts @@ -1,11 +1,17 @@ +/** + * 侧边栏 - 阅读 + */ + import * as vscode from 'vscode'; import { COMMANDS } from '../constants'; import bookManager, { TreeNode } from './bookManager'; import favoritesManager from '../utils/favoritesManager'; +import { ChapterItem, Rule } from '@any-reader/core'; export class BookProvider implements vscode.TreeDataProvider { readonly _onDidChangeTreeData = new vscode.EventEmitter(); readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + readonly cache = new Map(); refresh(): void { this._onDidChangeTreeData.fire(undefined); @@ -28,13 +34,34 @@ export class BookProvider implements vscode.TreeDataProvider { }; } - getChildren(element?: TreeNode): Promise { + // 获取缓存列表 + getChildrenCache(ruleId: string, url: string) { + const key = `${ruleId}@${url}`; + return this.cache.get(key) || this.cache.get('_') || []; + } + + async getChildren(element?: TreeNode): Promise { if (!element) { - return bookManager.getChildren(); + const items = await bookManager.getChildren(); + return items; } else { - return bookManager.getChapter(element); + const chapters = await bookManager.getChapter(element); + this.cache.set(`${element.rule.id}@${element.url}`, chapters); + return chapters; } } + + setChapters(chapterItems: ChapterItem[], rule: Rule, url: string) { + this.cache.clear(); + bookManager.list = chapterItems.map((chapterItem: ChapterItem) => ({ + rule, + type: 2, + data: chapterItem, + url + })); + this.cache.set('_', bookManager.list); + this.refresh(); + } } export default new BookProvider(); diff --git a/packages/vscode/src/treeview/bookManager.ts b/packages/vscode/src/treeview/bookManager.ts index 8c7ca27a..56b4c895 100644 --- a/packages/vscode/src/treeview/bookManager.ts +++ b/packages/vscode/src/treeview/bookManager.ts @@ -6,6 +6,7 @@ export interface TreeNode { rule: Rule; type: number; // 1=SearchItem 2=ChapterItem data: SearchItem | ChapterItem; + url: string; } class BookManager implements vscode.Disposable { @@ -35,11 +36,12 @@ class BookManager implements vscode.Disposable { const rm = new RuleManager(rule); const list = await rm.search(keyword); - this.list = list.map((searchItem: unknown) => { + this.list = list.map((searchItem: SearchItem) => { return { rule, type: 1, - data: searchItem + data: searchItem, + url: searchItem.url } as TreeNode; }); } @@ -55,6 +57,7 @@ class BookManager implements vscode.Disposable { list.map((e: any) => ({ type: 2, rule: tn.rule, + url: tn.data.url, data: e })) ); diff --git a/packages/vscode/src/treeview/favorites.ts b/packages/vscode/src/treeview/favorites.ts index 9ac79422..d8a0a7fc 100644 --- a/packages/vscode/src/treeview/favorites.ts +++ b/packages/vscode/src/treeview/favorites.ts @@ -1,3 +1,7 @@ +/** + * 侧边栏 - 收藏 + */ + import * as vscode from 'vscode'; import { COMMANDS } from '../constants'; import { RecordFileRow } from '../utils/RecordFile'; diff --git a/packages/vscode/src/treeview/history.ts b/packages/vscode/src/treeview/history.ts index 77085ffb..b814c002 100644 --- a/packages/vscode/src/treeview/history.ts +++ b/packages/vscode/src/treeview/history.ts @@ -1,3 +1,7 @@ +/** + * 侧边栏 - 历史记录 + */ + import * as vscode from 'vscode'; import { COMMANDS } from '../constants'; import { RecordFileRow } from '../utils/RecordFile'; diff --git a/packages/vscode/src/treeview/localBook.ts b/packages/vscode/src/treeview/localBook.ts index de2ec74b..3b322988 100644 --- a/packages/vscode/src/treeview/localBook.ts +++ b/packages/vscode/src/treeview/localBook.ts @@ -5,6 +5,7 @@ import { COMMANDS } from '../constants'; class TreeDataProvider implements vscode.TreeDataProvider { readonly _onDidChangeTreeData = new vscode.EventEmitter(); readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + readonly cache = new Map(); async refresh(): Promise { this._onDidChangeTreeData.fire(undefined); @@ -42,12 +43,20 @@ class TreeDataProvider implements vscode.TreeDataProvider { }; } + // 获取缓存列表 + getChildrenCache(path: string) { + return this.cache.get(path) || []; + } + // 获取目录 - getChildren(item?: TreeNode): Promise { + async getChildren(item?: TreeNode): Promise { if (!item) { + this.cache.clear(); return getBookList(); } else { - return getChapter(item as BookFile); + const chapters = await getChapter(item.path).catch(() => []); + this.cache.set(item.path, chapters); + return chapters; } } } diff --git a/packages/vscode/src/treeview/source.ts b/packages/vscode/src/treeview/source.ts index 74a39587..9a05d5c6 100644 --- a/packages/vscode/src/treeview/source.ts +++ b/packages/vscode/src/treeview/source.ts @@ -1,3 +1,7 @@ +/** + * 侧边栏 - 规则 + */ + import * as vscode from 'vscode'; import { Rule, ContentType } from '@any-reader/core'; import * as ruleFileManager from '../utils/ruleFileManager'; diff --git a/packages/vscode/src/utils/easyPostMessage.ts b/packages/vscode/src/utils/easyPostMessage.ts index b99a9f60..8ffd9cad 100644 --- a/packages/vscode/src/utils/easyPostMessage.ts +++ b/packages/vscode/src/utils/easyPostMessage.ts @@ -1,3 +1,7 @@ +/** + * 消息通信, 处理 webview 和 vscode + */ + import { Webview } from 'vscode'; import { IAdapter, IMessage } from 'easy-post-message'; diff --git a/packages/vscode/src/utils/sleep.ts b/packages/vscode/src/utils/sleep.ts new file mode 100644 index 00000000..b1d0189d --- /dev/null +++ b/packages/vscode/src/utils/sleep.ts @@ -0,0 +1,8 @@ +/** + * 延时 + * @param {number} time 时间 + * @returns + */ +export function sleep(time: number) { + return new Promise((resolve) => setTimeout(resolve, time)); +} diff --git a/packages/vscode/src/webview/index.ts b/packages/vscode/src/webview/index.ts index 9f034912..48a4efec 100644 --- a/packages/vscode/src/webview/index.ts +++ b/packages/vscode/src/webview/index.ts @@ -1,14 +1,31 @@ +/** + * 处理 webview 相关逻辑 + */ + import * as path from 'path'; import * as fs from 'fs'; +import { stringify } from 'qs'; import * as vscode from 'vscode'; import * as EasyPostMessage from 'easy-post-message'; -import { Rule, RuleManager } from '@any-reader/core'; +import { ContentType, Rule, RuleManager } from '@any-reader/core'; +import type { BookChapter } from '@any-reader/shared/localBookManager'; +import * as localBookManager from '@any-reader/shared/localBookManager'; +import { config } from '../config'; import { COMMANDS } from '../constants'; import * as ruleFileManager from '../utils/ruleFileManager'; import { createAdapter } from '../utils/easyPostMessage'; -import { TreeNode } from '../treeview/bookManager'; import favoritesProvider from '../treeview/favorites'; +import localBook from '../treeview/localBook'; +import book from '../treeview/book'; import favoritesManager from '../utils/favoritesManager'; +import { sleep } from '../utils/sleep'; +import bookManager, { TreeNode } from '../treeview/bookManager'; + +export enum WebViewFileBook { + None = 0, + WebViewFileOnline = 1, + WebViewFileLocal = 2 +} function success(data: any, msg = '') { return { @@ -18,12 +35,30 @@ function success(data: any, msg = '') { }; } +/** + * 转换为 BookChapter + * @param filePath 文件路径 + * @param chapterPath 章节路径 + * @returns + */ +function toBookChapter(filePath: string, chapterPath: string): BookChapter { + return { + file: { + path: filePath, + type: localBookManager.getBookType(filePath), + name: '' + }, + name: '', + path: chapterPath + }; +} + export class WebView { - private mSearchToken: any; private webviewPanel?: vscode.WebviewPanel; private context: vscode.ExtensionContext; private isVue = false; private pm!: any; + private webViewFileBook: WebViewFileBook = WebViewFileBook.None; constructor(context: vscode.ExtensionContext) { this.context = context; @@ -42,6 +77,8 @@ export class WebView { this.initWebviewPanel(title); if (!this.isVue) { this.webviewPanel!.webview.html = this.getWebViewContent(path.join('template-dist', 'index.html')); + // TODO: 第一次打开是没有就绪 + await sleep(500); } this.pm.emit('router.push', routePath, this.webviewPanel?.webview); this.isVue = true; @@ -56,87 +93,204 @@ export class WebView { this.webviewPanel!.reveal(); } + openWebviewPanelText(title: string, content: string) { + if (!content) { + return; + } + if (config.app.get('hideImage', false)) { + content = content.replace(//gim, ''); + } + const injectedHtml = config.app.get('injectedHtml', ''); + const css = ` + html, + body { + margin: 0; + padding: 0; + height: 100%; + width: 100%; + } + body { + font-size: 1em; + } + p { + margin: 0; + padding: 0; + } + `; + this.openWebviewPanel( + title, + `${injectedHtml}
${content}
` + ); + } + + // 获取章节 + async _vscGetChapter(data: any) { + const { ruleId, data: searchItem } = data; + const rule = await ruleFileManager.findById(ruleId); + vscode.commands.executeCommand( + COMMANDS.getChapter, + { + ...searchItem, + ruleId: rule.id + }, + { + saveHistory: searchItem + } + ); + } + + // 编辑规则 + _vscEditBookSource() { + vscode.commands.executeCommand(COMMANDS.editBookSource); + } + // 获取所有规则 - onBookSource() { + _rules() { return success(ruleFileManager.list()); } // 获取单个规则 - onGetRule(data: Rule) { + _getRuleById(data: Rule) { const rules = ruleFileManager.list(); return success(rules.find((e) => e.id === data.id)); } // 添加规则 - onAddRule(data: Rule) { + _createRule(data: Rule) { ruleFileManager.update(data); } // 更新规则 - async onUpdateRule(data: Rule) { + async _updateRule(data: Rule) { await ruleFileManager.update(data); return success(data); } // 搜索 - async onSearchByRuleId({ ruleId, keyword }: { ruleId: string; keyword: string }) { + async _searchByRuleId({ ruleId, keyword }: { ruleId: string; keyword: string }) { const rule = await ruleFileManager.findById(ruleId); const analyzer = new RuleManager(rule); return success(await analyzer.search(keyword).catch(() => [])); } - // 获取章节 - async onGetChapter(data: any) { - const { ruleId, data: searchItem } = data; - const rule = await ruleFileManager.findById(ruleId); - vscode.commands.executeCommand( - COMMANDS.getChapter, - { - ...searchItem, - ruleId: rule.id - }, - { - saveHistory: searchItem - } - ); - } - // 获取分类 - async onDiscoverMap(data: any) { + async _discoverMap(data: any) { const rule = await ruleFileManager.findById(data.ruleId); const ruleManager = new RuleManager(rule); return success(await ruleManager.discoverMap()); } // 获取分类下内容 - async onDiscover(data: any) { + async _discover(data: any) { const rule = await ruleFileManager.findById(data.ruleId); const ruleManager = new RuleManager(rule); return success(await ruleManager.discover(data.data.value)); } - // 编辑规则 - onEditBookSource() { - vscode.commands.executeCommand(COMMANDS.editBookSource); - } - // 获取收藏列表 - async onGetFavoritesList() { + async _getFavorites() { return success(await favoritesManager.list()); } - async onStar({ data, ruleId }: any) { + // 收藏 + async _star({ data, ruleId }: any) { await favoritesManager.add(data, await ruleFileManager.findById(ruleId)); favoritesProvider.refresh(); return success(true); } - async onUnstar({ data, ruleId }: any) { + // 取消收藏 + async _unstar({ data, ruleId }: any) { await favoritesManager.del(data, await ruleFileManager.findById(ruleId)); favoritesProvider.refresh(); return success(true); } + // 获取章节列表 + async _getChapter({ filePath = '', ruleId = undefined } = {}) { + if (ruleId) { + const rule = await ruleFileManager.findById(ruleId); + const rm = new RuleManager(rule); + const list = await rm.getChapter(filePath); + return success( + list.map((e) => ({ + ...e, + name: e.name, + path: e.url + })) + ); + } + // 本地 + return success(await localBookManager.getChapter(filePath)); + } + + // 获取章节内容 + async _content({ filePath, chapterPath, ruleId }: any) { + // 在线 + if (ruleId) { + const rule = await ruleFileManager.findById(ruleId); + const rm = new RuleManager(rule); + const content = await rm.getContent(chapterPath); + let text = ''; + if (rule.contentType === ContentType.MANGA) { + text = content.map((src) => ``).join(''); + } else { + text = content.join(''); + } + return success({ + content: text + // ...this.getOtherChapter(filePath, chapterPath, ruleId) + }); + } + // 本地 + const content = await localBookManager.getContent(toBookChapter(filePath, chapterPath)); + return success({ + content + // ...this.getOtherChapter(filePath, chapterPath) + }); + } + + /** + * 获取章节 + * @deprecated 这里处理起来有点繁琐, 也许应该把全部章节数据存放在Webview + */ + getOtherChapter(filePath: string, chapterPath: string, ruleId?: string) { + if (this.webViewFileBook === WebViewFileBook.WebViewFileOnline) { + const chapters = book.getChildrenCache(ruleId ?? '', filePath); + if (!chapters.length) { + return {}; + } + const idx = chapters.findIndex((e: any) => e.data.url === chapterPath); + const lastChapter = idx === 0 ? null : chapters[idx - 1]; + const nextChapterIdx = idx + 1; + const nextChapter = chapters.length - 1 < nextChapterIdx ? null : chapters[nextChapterIdx]; + return { + lastChapter: lastChapter?.data?.url, + nextChapter: nextChapter?.data?.url + }; + } else if (this.webViewFileBook === WebViewFileBook.WebViewFileLocal) { + const chapters = localBook.getChildrenCache(filePath); + if (!chapters.length) { + return {}; + } + const idx = chapters.findIndex((e: any) => e.path === chapterPath); + const lastChapter = idx === 0 ? null : chapters[idx - 1]; + const nextChapterIdx = idx + 1; + const nextChapter = chapters.length - 1 < nextChapterIdx ? null : chapters[nextChapterIdx]; + return { + lastChapter: lastChapter?.path, + nextChapter: nextChapter?.path + }; + } + + return {}; + } + + /** + * 初始化 WebView + * @param {string} title 标题 + */ initWebviewPanel(title: string) { if (!this.webviewPanel) { this.webviewPanel = vscode.window.createWebviewPanel('any-reader', title, vscode.ViewColumn.One, { @@ -154,24 +308,91 @@ export class WebView { // @ts-ignore this.pm = new EasyPostMessage(createAdapter(this.webviewPanel.webview)); + // 消息通信, 为了以后兼容 XHR, 模板分离出独立可运行浏览器版本 + // vsc - this.pm.answer('post@vscode/getChapter', this.onGetChapter.bind(this)); - this.pm.answer('get@vscode/editBookSource', this.onEditBookSource.bind(this)); - - this.pm.answer('get@discoverMap', this.onDiscoverMap.bind(this)); - this.pm.answer('get@getFavorites', this.onGetFavoritesList.bind(this)); - this.pm.answer('post@discover', this.onDiscover.bind(this)); - this.pm.answer('post@star', this.onStar.bind(this)); - this.pm.answer('post@unstar', this.onUnstar.bind(this)); - - this.pm.answer('get@rules', this.onBookSource.bind(this)); - this.pm.answer('get@getRuleById', this.onGetRule.bind(this)); - this.pm.answer('post@createRule', this.onAddRule.bind(this)); - this.pm.answer('post@updateRule', this.onUpdateRule.bind(this)); - - this.pm.answer('post@searchByRuleId', this.onSearchByRuleId.bind(this)); - } else { - this.webviewPanel.title = title; + this.pm.answer('post@vscode/getChapter', this._vscGetChapter.bind(this)); + this.pm.answer('get@vscode/editBookSource', this._vscEditBookSource.bind(this)); + + this.pm.answer('get@discoverMap', this._discoverMap.bind(this)); + this.pm.answer('get@getFavorites', this._getFavorites.bind(this)); + this.pm.answer('post@discover', this._discover.bind(this)); + this.pm.answer('post@star', this._star.bind(this)); + this.pm.answer('post@unstar', this._unstar.bind(this)); + this.pm.answer('get@rules', this._rules.bind(this)); + this.pm.answer('get@getRuleById', this._getRuleById.bind(this)); + this.pm.answer('post@createRule', this._createRule.bind(this)); + this.pm.answer('post@updateRule', this._updateRule.bind(this)); + this.pm.answer('post@searchByRuleId', this._searchByRuleId.bind(this)); + this.pm.answer('post@content', this._content.bind(this)); + this.pm.answer('post@getChapter', this._getChapter.bind(this)); } + this.webviewPanel.title = title; + } + + // 获取文章详情 + async getContent(article: TreeNode) { + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Window, + title: 'loading...', + cancellable: false + }, + async () => { + const contentType = article.rule.contentType; + // 视频 + if (contentType === ContentType.VIDEO) { + const textArr = await bookManager.getContent(article); + if (!textArr?.length) { + vscode.window.showWarningMessage('empty content'); + } + this.navigateTo('/player?url=' + textArr[0]); + return; + } + + // TODO: 漫画模板待优化 + // 小说 + if ([ContentType.MANGA, ContentType.NOVEL, ContentType.NOVELMORE].includes(contentType)) { + this.webViewFileBook = WebViewFileBook.WebViewFileOnline; + this.navigateTo( + '/content?' + + stringify({ + ruleId: article.rule.id, + filePath: article.url, + chapterPath: article.data.url + }) + // article.data.name + ); + } + } + ); + } + + // 阅读本地书籍 + async getContentLocalBook(item: BookChapter) { + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Window, + title: 'loading...', + cancellable: false + }, + async () => { + if (item.file.type === localBookManager.BOOK_TYPE.EPUB) { + this.webViewFileBook = WebViewFileBook.WebViewFileLocal; + this.navigateTo( + '/content?' + + stringify({ + filePath: item.file.path, + chapterPath: item.path + }) + ); + } else { + // TODO: TXT 数据太大, 分章处理? + const openPath = vscode.Uri.file(item.file.path); + // vscode.window.showTextDocument(openPath); + vscode.commands.executeCommand('vscode.open', openPath); + } + } + ); } } diff --git a/packages/web/.eslintrc.cjs b/packages/web/.eslintrc.cjs index 43a35855..a691f487 100644 --- a/packages/web/.eslintrc.cjs +++ b/packages/web/.eslintrc.cjs @@ -14,6 +14,7 @@ module.exports = { ecmaVersion: 'latest' }, rules: { - 'vue/multi-word-component-names': 0 + 'vue/multi-word-component-names': 0, + 'vue/no-v-html': 0 } }; diff --git a/packages/web/src/api/index.ts b/packages/web/src/api/index.ts index c5a44f37..d1a6258f 100644 --- a/packages/web/src/api/index.ts +++ b/packages/web/src/api/index.ts @@ -89,3 +89,24 @@ export function searchByRuleId(data: { ruleId: string; keyword: string }) { data }); } + +// 获取内容 +export function getContent(data: any) { + return request({ + method: 'post', + url: 'content', + data + }); +} + +// 获取章节 +export function getChapter(filePath: string, ruleId?: string) { + return request({ + method: 'post', + url: 'getChapter', + data: { + filePath, + ruleId + } + }); +} diff --git a/packages/web/src/pages/content/index.vue b/packages/web/src/pages/content/index.vue new file mode 100644 index 00000000..109f1757 --- /dev/null +++ b/packages/web/src/pages/content/index.vue @@ -0,0 +1,123 @@ + + + + + diff --git a/packages/web/src/router/index.ts b/packages/web/src/router/index.ts index c9214ca0..75220be9 100644 --- a/packages/web/src/router/index.ts +++ b/packages/web/src/router/index.ts @@ -30,6 +30,11 @@ const router = createRouter({ { path: '/discover', component: () => import('@/pages/discover/index.vue') + }, + { + path: '/content', + name: 'content', + component: () => import('@/pages/content/index.vue') } ] }); diff --git a/packages/web/src/stores/chapters.ts b/packages/web/src/stores/chapters.ts new file mode 100644 index 00000000..4872784b --- /dev/null +++ b/packages/web/src/stores/chapters.ts @@ -0,0 +1,28 @@ +import { getChapter } from '@/api'; + +interface Chapters { + name: string; + path: string; +} + +export const useChaptersStore = defineStore('chapters', () => { + const chapters = ref([]); + + /** + * 获取 Chapters + * @param filePath + */ + async function getChapters(filePath: string, ruleId?: string) { + chapters.value = []; + const res = await getChapter(filePath, ruleId).catch(() => {}); + if (res?.code === 0) { + const rows = res?.data || []; + chapters.value = rows; + } + } + + return { + getChapters, + chapters + }; +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4cd78617..854d3993 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + importers: .: @@ -223,6 +227,9 @@ importers: fs-extra: specifier: ^11.1.1 version: 11.1.1 + qs: + specifier: ^6.12.1 + version: 6.12.1 uuid: specifier: ^9.0.1 version: 9.0.1 @@ -9804,6 +9811,13 @@ packages: side-channel: 1.0.6 dev: false + /qs@6.12.1: + resolution: {integrity: sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.6 + dev: false + /queue-microtask@1.2.2: resolution: {integrity: sha512-dB15eXv3p2jDlbOiNLyMabYg1/sXvppd8DP2J3EOCQ0AkuSXCW2tP7mnVouVLJKgUMY6yP0kcQDVpLCN13h4Xg==} dev: true @@ -10694,6 +10708,30 @@ packages: webpack: 5.73.0(esbuild@0.20.2) dev: true + /terser-webpack-plugin@5.3.10(webpack@5.73.0): + resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + dependencies: + '@jridgewell/trace-mapping': 0.3.20 + jest-worker: 27.5.1 + schema-utils: 3.3.0 + serialize-javascript: 6.0.2 + terser: 5.27.0 + webpack: 5.73.0(webpack-cli@4.10.0) + dev: true + /terser@5.27.0: resolution: {integrity: sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==} engines: {node: '>=10'} @@ -11772,7 +11810,7 @@ packages: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(esbuild@0.20.2)(webpack@5.73.0) + terser-webpack-plugin: 5.3.10(webpack@5.73.0) watchpack: 2.4.0 webpack-cli: 4.10.0(webpack@5.73.0) webpack-sources: 3.2.3