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:
Yuhang Wu 2026-08-21 14:20:24 +08:00
parent 6cc048c7b9
commit 8770cd7337
4 changed files with 152 additions and 73 deletions

119
README.md
View File

@ -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/ # Piniasetting 持久化)
├── types/ # 全局类型定义
└── views/ # Home / BookDetail / Reader / Setting 等页面
src-tauri/
└── src/
├── commands/ # Tauri 命令book_*、chapter_*
├── services/ # SQLite 初始化、TXT 解析
└── models/ # 数据模型
```
## 路线图 ## 路线图
### v0.1.0MVP - 老板键(系统级全局快捷键,一键显隐)
- 系统托盘与伪装图标
- TXT 导入 + 编码自动检测 - 阅读进度自动保存与续读
- 书架列表 + 删除 - EPUB 解析支持
- 分页阅读 + 字体 / 行距设置 - 主题切换实际应用(浅色 / 深色 / 跟随系统)
- 老板键(显隐) - 幽灵模式(鼠标穿透 + 极小悬浮窗)
- 窗口置顶 + 透明度调节
- 阅读进度保存
### v1.0.0
- 幽灵模式(鼠标穿透 + 悬停切换)
- 系统托盘 + 伪装图标
- 设置面板
- 窗口尺寸记忆
- 安装包Windows .msi / macOS .dmg
### v1.1.0
- EPUB 导入支持
- 章节手动跳转
- 阅读时长统计
- 主题切换IDE 伪装主题)
### v2.0(未来)
- 多设备进度同步WebDAV / 局域网) - 多设备进度同步WebDAV / 局域网)
- 语音朗读TTS - 语音朗读TTS、书摘 / 笔记
- 书摘 / 笔记
## 免责声明 ## 免责声明

View File

@ -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']
} }

View File

@ -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>

View File

@ -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>