v0.1.0 — MVP
✨ 功能亮点 书籍导入与解析 本地 TXT 文件导入(系统文件选择对话框) 自动检测 GBK / UTF-8 等常见中文编码 自动分章:识别「第 X 章」「Chapter N」「楔子/序章/前言/引子/后记/尾声/番外」等格式,无法识别时整书作为单章导入 自动统计章节数与总字数 书架管理 封面网格展示(本地封面 / 默认封面) 按书名模糊搜索 右键菜单编辑与删除(二次确认) 空状态一键导入引导 书籍详情 作者、章节数、总字数、导入时间、上次阅读、阅读进度展示 简介悬停浮层,超长简介可滚动 章节列表分页展示(20 / 50 / 100 每页) 分页阅读 点击页面左右半屏翻页;支持 ↑↓、PgUp/PgDn、空格、←→ 键盘操作 上一章 / 下一章切换与边界提示 阅读外观实时调整:字体大小、字体颜色、背景颜色与透明度、窗口大小 阅读窗口可拖拽,支持窗口阴影开关 设置中心 外观(窗口阴影)、阅读设置(字体/颜色/背景/窗口)、快捷键说明、关于 设置通过 Pinia 持久化,跨会话保存 界面与体验 自定义标题栏(Windows / macOS),仅关闭与最小化 全局加载状态、圆角玻璃质感界面、窗口置顶 📦 技术栈 Tauri 2 + Rust(sqlx + SQLite、chardetng 编码检测) Vue 3 + TypeScript + Vite Naive UI + @vicons 图标 Pinia(持久化) 🧩 支持的平台 Windows / macOS(构建产物为本地安装包) 🗓️ 规划中 老板键(全局快捷键显隐)、系统托盘 阅读进度自动保存与续读 EPUB 解析、主题切换实际应用 幽灵模式(鼠标穿透)、多设备同步、TTS ⚠️ 免责声明 本项目仅支持本地文件导入,不内置任何在线书源或下载功能,请使用您拥有合法权利的书籍内容。
This commit is contained in:
parent
6cc048c7b9
commit
8770cd7337
119
README.md
119
README.md
|
|
@ -1,43 +1,53 @@
|
||||||
# 幽灵阅读器 · Stealth Reader
|
# 幽灵阅读器 · Stealth Reader
|
||||||
|
|
||||||
一款永远浮在屏幕角落、能一键隐身、看起来像系统通知的本地小说阅读器。
|
一款安静、本地优先的桌面小说阅读器。轻量、纯本地、无账号、无广告,专注「好好读一本书」。
|
||||||
|
|
||||||
核心不是「读小说」,而是「安全地读小说」——解决职场场景下「想摸鱼但怕被发现」的焦虑。
|
## 已实现功能
|
||||||
|
|
||||||
## 定位
|
### 书籍导入与解析
|
||||||
|
- **本地 TXT 导入**:通过系统文件选择对话框导入,无需任何在线源
|
||||||
|
- **编码自动检测**:基于 `chardetng` 自动识别 GBK / UTF-8 等常见中文编码
|
||||||
|
- **自动分章**:正则识别「第 X 章」「Chapter N」「楔子 / 序章 / 前言 / 引子 / 后记 / 尾声 / 番外」等格式;无法识别时整本书作为单章「正文」导入
|
||||||
|
- **元数据统计**:自动统计章节数与总字数
|
||||||
|
|
||||||
桌面端本地小说阅读器,核心卖点是「职场隐身模式」。
|
### 书架管理
|
||||||
|
- 封面网格展示(本地封面文件 / 默认封面)
|
||||||
|
- 按书名模糊搜索
|
||||||
|
- 右键菜单编辑(书名 / 作者 / 简介)与删除(二次确认)
|
||||||
|
- 空状态引导一键导入
|
||||||
|
|
||||||
- 目标平台:Windows / macOS / Linux(基于 Tauri 跨平台)
|
### 书籍详情
|
||||||
- 核心价值:轻量 + 本地 + 隐蔽
|
- 封面 + 作者 / 章节数 / 总字数 / 导入时间 / 上次阅读时间 / 阅读进度
|
||||||
|
- 简介悬停浮层(超长简介内可滚动)
|
||||||
|
- 章节列表分页展示(每页 20 / 50 / 100 可选)
|
||||||
|
|
||||||
## 特性
|
### 分页阅读
|
||||||
|
- 点击页面左右半屏翻页,或使用键盘(`↑↓` / `PgUp` / `PgDn` / 空格 / `←→`)
|
||||||
|
- 上一章 / 下一章快速切换,边界提示
|
||||||
|
- 阅读外观实时生效:字体大小、字体颜色、背景颜色与透明度、窗口大小
|
||||||
|
- 阅读窗口可拖拽,支持窗口阴影开关
|
||||||
|
|
||||||
- **本地导入**:支持 TXT 导入,自动检测 GBK / UTF-8 编码
|
### 设置中心
|
||||||
- **自动分章**:正则识别「第 X 章」等常见格式,自动拆分章节
|
- **外观**:窗口阴影开关(主题模式选项已预留)
|
||||||
- **分页阅读**:按键翻页(↑↓ / PgUp / PgDn),无滚动条,像读实体书
|
- **阅读设置**:字体大小、字体颜色、背景颜色与透明度、阅读窗口大小、窗口阴影
|
||||||
- **阅读进度持久化**:自动保存进度,下次打开继续读
|
- **快捷键**:按键说明参考
|
||||||
- **老板键**:系统级全局快捷键,一键瞬间隐身,任务栏无图标
|
- **关于**:版本信息
|
||||||
- **幽灵模式**:窗口透明 + 鼠标穿透 + 置顶,真正融入桌面
|
|
||||||
- **系统托盘**:支持伪装图标,静默驻留后台
|
|
||||||
- **零打扰**:无账号、无广告、无在线同步,纯本地
|
|
||||||
|
|
||||||
## 四种预设模式
|
### 窗口与界面
|
||||||
|
- 自定义标题栏(Windows / macOS):仅保留关闭与最小化按钮,标题栏区域可拖拽窗口
|
||||||
| 模式 | 尺寸 | 透明度 | 鼠标穿透 | 交互 | 适用场景 |
|
- 窗口支持透明背景、置顶显示、圆角毛玻璃外观
|
||||||
|------|------|--------|---------|------|---------|
|
- 设置通过 Pinia + 持久化插件跨会话保存
|
||||||
| 专注模式 | 600×400 | 100% | 关闭 | 完整交互 | 午休 / 独处 |
|
|
||||||
| 幽灵模式 | 300×150 | 30% | 开启 | 无(纯看) | 边工作边偷看 |
|
|
||||||
| 隐身模式 | 隐藏 | 0% | - | 无 | 老板来了 |
|
|
||||||
| 迷你模式 | 200×100 | 50% | 可选 | 仅翻页 | 会议中 |
|
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
- [Tauri 2](https://tauri.app/) — 跨平台桌面壳与窗口级 API
|
| 层 | 技术 |
|
||||||
- [Vue 3](https://vuejs.org/) + TypeScript + Vite
|
|----|------|
|
||||||
- [Naive UI](https://www.naiveui.com/) — 组件库
|
| 桌面壳 | [Tauri 2](https://tauri.app/) |
|
||||||
- [Pinia](https://pinia.vuejs.org/) — 状态管理(persistedstate 持久化)
|
| 前端 | [Vue 3](https://vuejs.org/) + TypeScript + [Vite](https://vite.dev/) |
|
||||||
- Rust — 后端能力(编码检测、全局快捷键、系统托盘等)
|
| 组件库 | [Naive UI](https://www.naiveui.com/) + @vicons 图标 |
|
||||||
|
| 状态 | [Pinia](https://pinia.vuejs.org/) + `pinia-plugin-persistedstate` |
|
||||||
|
| 后端 | Rust:`sqlx` + SQLite、`chardetng`(编码检测)、`regex`(分章) |
|
||||||
|
| 插件 | `tauri-plugin-dialog`(文件选择)、`tauri-plugin-store`、`tauri-plugin-opener` |
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
|
|
@ -62,37 +72,34 @@ pnpm dev # 启动 Vite 开发服务器(http://localhost:1420)
|
||||||
pnpm build # 类型检查 + 构建前端产物
|
pnpm build # 类型检查 + 构建前端产物
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── api/ # Tauri invoke 封装(书籍 / 章节)
|
||||||
|
├── components/ # BookCover、GlobalLoading
|
||||||
|
├── hooks/ # useLoading
|
||||||
|
├── layout/ # 主布局与自定义标题栏(Windows / macOS)
|
||||||
|
├── stores/ # Pinia(setting 持久化)
|
||||||
|
├── types/ # 全局类型定义
|
||||||
|
└── views/ # Home / BookDetail / Reader / Setting 等页面
|
||||||
|
src-tauri/
|
||||||
|
└── src/
|
||||||
|
├── commands/ # Tauri 命令(book_*、chapter_*)
|
||||||
|
├── services/ # SQLite 初始化、TXT 解析
|
||||||
|
└── models/ # 数据模型
|
||||||
|
```
|
||||||
|
|
||||||
## 路线图
|
## 路线图
|
||||||
|
|
||||||
### v0.1.0(MVP)
|
- 老板键(系统级全局快捷键,一键显隐)
|
||||||
|
- 系统托盘与伪装图标
|
||||||
- TXT 导入 + 编码自动检测
|
- 阅读进度自动保存与续读
|
||||||
- 书架列表 + 删除
|
- EPUB 解析支持
|
||||||
- 分页阅读 + 字体 / 行距设置
|
- 主题切换实际应用(浅色 / 深色 / 跟随系统)
|
||||||
- 老板键(显隐)
|
- 幽灵模式(鼠标穿透 + 极小悬浮窗)
|
||||||
- 窗口置顶 + 透明度调节
|
|
||||||
- 阅读进度保存
|
|
||||||
|
|
||||||
### v1.0.0
|
|
||||||
|
|
||||||
- 幽灵模式(鼠标穿透 + 悬停切换)
|
|
||||||
- 系统托盘 + 伪装图标
|
|
||||||
- 设置面板
|
|
||||||
- 窗口尺寸记忆
|
|
||||||
- 安装包(Windows .msi / macOS .dmg)
|
|
||||||
|
|
||||||
### v1.1.0
|
|
||||||
|
|
||||||
- EPUB 导入支持
|
|
||||||
- 章节手动跳转
|
|
||||||
- 阅读时长统计
|
|
||||||
- 主题切换(IDE 伪装主题)
|
|
||||||
|
|
||||||
### v2.0(未来)
|
|
||||||
|
|
||||||
- 多设备进度同步(WebDAV / 局域网)
|
- 多设备进度同步(WebDAV / 局域网)
|
||||||
- 语音朗读(TTS)
|
- 语音朗读(TTS)、书摘 / 笔记
|
||||||
- 书摘 / 笔记
|
|
||||||
|
|
||||||
## 免责声明
|
## 免责声明
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,39 @@ declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
BookCover: typeof import('./../components/BookCover.vue')['default']
|
BookCover: typeof import('./../components/BookCover.vue')['default']
|
||||||
GlobalLoading: typeof import('./../components/GlobalLoading.vue')['default']
|
GlobalLoading: typeof import('./../components/GlobalLoading.vue')['default']
|
||||||
|
NAnchor: typeof import('naive-ui')['NAnchor']
|
||||||
|
NAnchorLink: typeof import('naive-ui')['NAnchorLink']
|
||||||
|
NButton: typeof import('naive-ui')['NButton']
|
||||||
|
NColorPicker: typeof import('naive-ui')['NColorPicker']
|
||||||
|
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||||
|
NDescriptions: typeof import('naive-ui')['NDescriptions']
|
||||||
|
NDescriptionsItem: typeof import('naive-ui')['NDescriptionsItem']
|
||||||
|
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||||
|
NDivider: typeof import('naive-ui')['NDivider']
|
||||||
|
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||||
|
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||||
|
NForm: typeof import('naive-ui')['NForm']
|
||||||
|
NFormItem: typeof import('naive-ui')['NFormItem']
|
||||||
|
NGi: typeof import('naive-ui')['NGi']
|
||||||
|
NGrid: typeof import('naive-ui')['NGrid']
|
||||||
|
NIcon: typeof import('naive-ui')['NIcon']
|
||||||
|
NInput: typeof import('naive-ui')['NInput']
|
||||||
|
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||||
|
NLayout: typeof import('naive-ui')['NLayout']
|
||||||
|
NLayoutContent: typeof import('naive-ui')['NLayoutContent']
|
||||||
|
NLayoutHeader: typeof import('naive-ui')['NLayoutHeader']
|
||||||
|
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||||
|
NModal: typeof import('naive-ui')['NModal']
|
||||||
|
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||||
|
NPagination: typeof import('naive-ui')['NPagination']
|
||||||
|
NPopover: typeof import('naive-ui')['NPopover']
|
||||||
|
NRadioButton: typeof import('naive-ui')['NRadioButton']
|
||||||
|
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||||
|
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||||
|
NSpace: typeof import('naive-ui')['NSpace']
|
||||||
|
NSpin: typeof import('naive-ui')['NSpin']
|
||||||
|
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||||
|
NText: typeof import('naive-ui')['NText']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="glass-panel" :style="readerStyle" data-tauri-drag-region>
|
<div class="glass-panel" :style="readerStyle" data-tauri-drag-region @mousedown="onWindowMouseDown">
|
||||||
<div class="reading-wrap">
|
<div class="reading-wrap">
|
||||||
<div class="chapter-name">{{ chapter?.title || "加载中..." }}</div>
|
<div v-if="currentPage == 0" class="chapter-name">{{ chapter?.title || "加载中..." }}</div>
|
||||||
|
|
||||||
<div ref="pageEl" class="page-body" @click="onPageClick">
|
<div ref="pageEl" class="page-body" @click="onPageClick">
|
||||||
<div class="page-text">{{ currentText }}</div>
|
<div class="page-text">{{ currentText }}</div>
|
||||||
|
|
@ -9,19 +9,19 @@
|
||||||
|
|
||||||
<div class="page-nav">
|
<div class="page-nav">
|
||||||
<span class="page-info">{{ pageInfo }}</span>
|
<span class="page-info">{{ pageInfo }}</span>
|
||||||
<n-space justify="space-between" class="nav-btns">
|
<div class="nav-btns">
|
||||||
<n-space>
|
<n-space class="nav-group nav-group--left" :size="0">
|
||||||
<n-button size="small" text @click="switchChapter(-1)">上一章</n-button>
|
<n-button size="small" text @click="switchChapter(-1)">上一章</n-button>
|
||||||
<n-button size="small" text :disabled="currentPage <= 0" @click="prevPage">上一页</n-button>
|
<n-button size="small" text :disabled="currentPage <= 0" @click="prevPage">上一页</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
<n-button size="small" text @click="goBack">返回</n-button>
|
<n-button class="nav-back" size="small" text @click="goBack">返回</n-button>
|
||||||
<n-space>
|
<n-space class="nav-group nav-group--right" :size="0">
|
||||||
<n-button size="small" text :disabled="currentPage >= pages.length - 1" @click="nextPage">
|
<n-button size="small" text :disabled="currentPage >= pages.length - 1" @click="nextPage">
|
||||||
下一页
|
下一页
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button size="small" text @click="switchChapter(1)">下一章</n-button>
|
<n-button size="small" text @click="switchChapter(1)">下一章</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
</n-space>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -126,10 +126,23 @@ const onPageClick = (e: MouseEvent) => {
|
||||||
else nextPage()
|
else nextPage()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onWindowMouseDown = (e: MouseEvent) => {
|
||||||
|
const target = e.target as HTMLElement | null
|
||||||
|
if (target?.closest("button, a, input, textarea, select, [role='button']")) return
|
||||||
|
void appWindow.startDragging()
|
||||||
|
}
|
||||||
|
|
||||||
const onKeydown = (e: KeyboardEvent) => {
|
const onKeydown = (e: KeyboardEvent) => {
|
||||||
if (e.key === "ArrowLeft" || e.key === "PageUp") {
|
if (e.key === "ArrowLeft") {
|
||||||
|
e.preventDefault()
|
||||||
|
switchChapter(-1)
|
||||||
|
} else if (e.key === "ArrowRight") {
|
||||||
|
e.preventDefault()
|
||||||
|
switchChapter(1)
|
||||||
|
} else if (e.key === "ArrowUp" || e.key === "PageUp") {
|
||||||
|
e.preventDefault()
|
||||||
prevPage()
|
prevPage()
|
||||||
} else if (e.key === "ArrowRight" || e.key === "PageDown" || e.key === " ") {
|
} else if (e.key === "ArrowDown" || e.key === "PageDown" || e.key === " ") {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
nextPage()
|
nextPage()
|
||||||
}
|
}
|
||||||
|
|
@ -192,6 +205,7 @@ onUnmounted(() => {
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.glass-panel {
|
.glass-panel {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background: var(--reader-background-color);
|
background: var(--reader-background-color);
|
||||||
|
|
@ -232,7 +246,7 @@ onUnmounted(() => {
|
||||||
.chapter-name {
|
.chapter-name {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 22px;
|
font-size: 14px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--reader-font-color);
|
color: var(--reader-font-color);
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|
@ -260,8 +274,9 @@ onUnmounted(() => {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 4px;
|
||||||
margin-top: 16px;
|
margin-top: 8px;
|
||||||
|
padding-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-info {
|
.page-info {
|
||||||
|
|
@ -275,6 +290,30 @@ onUnmounted(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-btns {
|
.nav-btns {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-group {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-group--right {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btns :deep(.n-button) {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-back {
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
<span class="shortcut-keys">
|
<span class="shortcut-keys">
|
||||||
<template v-for="(key, i) in item.keys" :key="i">
|
<template v-for="(key, i) in item.keys" :key="i">
|
||||||
<kbd class="shortcut-key">{{ key }}</kbd>
|
<kbd class="shortcut-key">{{ key }}</kbd>
|
||||||
<span v-if="i < item.keys.length - 1" class="shortcut-sep">+</span>
|
<span v-if="i < item.keys.length - 1" class="shortcut-sep">/</span>
|
||||||
</template>
|
</template>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -19,10 +19,10 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const shortcuts = [
|
const shortcuts = [
|
||||||
{ action: "上一页", keys: ["←"] },
|
{ action: "上一页", keys: ["←", "PageUp"] },
|
||||||
{ action: "下一页", keys: ["→"] },
|
{ action: "下一页", keys: ["→", "PageDown"] },
|
||||||
{ action: "打开搜索", keys: ["Ctrl", "F"] },
|
{ action: "上一章", keys: ["←"] },
|
||||||
{ action: "打开设置", keys: ["Ctrl", ","] },
|
{ action: "下一章", keys: ["→"] },
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue