Compare commits

..

2 Commits

Author SHA1 Message Date
Yuhang Wu 7b06516cbe feat: 添加应用自动更新功能与手动更新入口
1.  新增@tauri-apps/plugin-updater和process插件依赖,配置更新相关权限与参数
2.  实现自动更新逻辑,应用启动时静默检查并更新
3.  在关于页面添加手动检查更新按钮,支持更新状态提示
4.  完善更新流程的异常处理与用户交互
2026-08-24 16:52:35 +08:00
Yuhang Wu 8f8f4691c4 feat(book-detail): 添加继续阅读按钮和界面预览图
1. 重构书籍详情页布局,新增继续阅读快速入口
2. 调整书籍简介展示逻辑,优化页面排版
3. 更新package.json版本号到0.2.0
4. 新增界面预览图片并添加到README文档
5. 优化阅读器章节标题显示逻辑
2026-08-24 15:46:08 +08:00
17 changed files with 958 additions and 90 deletions

View File

@ -38,6 +38,16 @@
- 窗口支持透明背景、置顶显示、圆角毛玻璃外观 - 窗口支持透明背景、置顶显示、圆角毛玻璃外观
- 设置通过 Pinia + 持久化插件跨会话保存 - 设置通过 Pinia + 持久化插件跨会话保存
## 界面预览
| 书架管理 | 书籍详情 |
| --- | --- |
| ![书架管理](images/1.png) | ![书籍章节详情](images/3.png) |
| 幽灵阅读模式 | 阅读设置 |
| --- | --- |
| ![幽灵阅读模式](images/4.png) | ![阅读设置](images/2.png) |
## 技术栈 ## 技术栈
| 层 | 技术 | | 层 | 技术 |

BIN
images/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

BIN
images/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
images/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
images/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

View File

@ -1,7 +1,7 @@
{ {
"name": "stealthreader", "name": "stealthreader",
"private": true, "private": true,
"version": "0.1.0", "version": "0.3.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@ -13,7 +13,9 @@
"@tauri-apps/api": "^2", "@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "^2.7.2", "@tauri-apps/plugin-dialog": "^2.7.2",
"@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-store": "~2.4.4", "@tauri-apps/plugin-store": "~2.4.4",
"@tauri-apps/plugin-updater": "^2.10.1",
"axios": "^1.19.0", "axios": "^1.19.0",
"dayjs": "^1.11.21", "dayjs": "^1.11.21",
"naive-ui": "^2.45.0", "naive-ui": "^2.45.0",

View File

@ -17,12 +17,18 @@ importers:
'@tauri-apps/plugin-opener': '@tauri-apps/plugin-opener':
specifier: ^2 specifier: ^2
version: 2.5.4 version: 2.5.4
'@tauri-apps/plugin-process':
specifier: ^2.3.1
version: 2.3.1
'@tauri-apps/plugin-store': '@tauri-apps/plugin-store':
specifier: ~2.4.4 specifier: ~2.4.4
version: 2.4.4 version: 2.4.4
'@tauri-apps/plugin-updater':
specifier: ^2.10.1
version: 2.10.1
axios: axios:
specifier: ^1.19.0 specifier: ^1.19.0
version: 1.19.0 version: 1.19.0(debug@4.4.3)
dayjs: dayjs:
specifier: ^1.11.21 specifier: ^1.11.21
version: 1.11.23 version: 1.11.23
@ -440,9 +446,15 @@ packages:
'@tauri-apps/plugin-opener@2.5.4': '@tauri-apps/plugin-opener@2.5.4':
resolution: {integrity: sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==} resolution: {integrity: sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==}
'@tauri-apps/plugin-process@2.3.1':
resolution: {integrity: sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==}
'@tauri-apps/plugin-store@2.4.4': '@tauri-apps/plugin-store@2.4.4':
resolution: {integrity: sha512-oxSMaj/QpVfJcBMYX5aOQV94fWvga0MwQMfD6TLlbK2dh+ShPWAzefd8HWXhvOKjPRJdGVAkW7ZGO76JzzjaDA==} resolution: {integrity: sha512-oxSMaj/QpVfJcBMYX5aOQV94fWvga0MwQMfD6TLlbK2dh+ShPWAzefd8HWXhvOKjPRJdGVAkW7ZGO76JzzjaDA==}
'@tauri-apps/plugin-updater@2.10.1':
resolution: {integrity: sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA==}
'@types/estree@1.0.9': '@types/estree@1.0.9':
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
@ -505,6 +517,11 @@ packages:
'@volar/typescript@2.4.28': '@volar/typescript@2.4.28':
resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
'@vue-macros/common@3.1.4': '@vue-macros/common@3.1.4':
resolution: {integrity: sha512-/5Fv+6DgIcM9ajY05ZmKBv+LMX1M9A0X+IUwDRVdt67ciw8OV9bvG2r34p3RiEadlsQybjhKPRKNXDC8Bp23cw==} resolution: {integrity: sha512-/5Fv+6DgIcM9ajY05ZmKBv+LMX1M9A0X+IUwDRVdt67ciw8OV9bvG2r34p3RiEadlsQybjhKPRKNXDC8Bp23cw==}
@ -1429,10 +1446,18 @@ snapshots:
dependencies: dependencies:
'@tauri-apps/api': 2.11.1 '@tauri-apps/api': 2.11.1
'@tauri-apps/plugin-process@2.3.1':
dependencies:
'@tauri-apps/api': 2.11.1
'@tauri-apps/plugin-store@2.4.4': '@tauri-apps/plugin-store@2.4.4':
dependencies: dependencies:
'@tauri-apps/api': 2.11.1 '@tauri-apps/api': 2.11.1
'@tauri-apps/plugin-updater@2.10.1':
dependencies:
'@tauri-apps/api': 2.11.1
'@types/estree@1.0.9': {} '@types/estree@1.0.9': {}
'@types/jsesc@2.5.1': {} '@types/jsesc@2.5.1': {}
@ -1482,11 +1507,13 @@ snapshots:
'@volar/source-map@2.4.28': {} '@volar/source-map@2.4.28': {}
'@volar/typescript@2.4.28': '@volar/typescript@2.4.28(typescript@5.6.3)':
dependencies: dependencies:
'@volar/language-core': 2.4.28 '@volar/language-core': 2.4.28
path-browserify: 1.0.1 path-browserify: 1.0.1
vscode-uri: 3.1.0 vscode-uri: 3.1.0
optionalDependencies:
typescript: 5.6.3
'@vue-macros/common@3.1.4(vue@3.5.41(typescript@5.6.3))': '@vue-macros/common@3.1.4(vue@3.5.41(typescript@5.6.3))':
dependencies: dependencies:
@ -1609,9 +1636,9 @@ snapshots:
asynckit@0.4.0: {} asynckit@0.4.0: {}
axios@1.19.0: axios@1.19.0(debug@4.4.3):
dependencies: dependencies:
follow-redirects: 1.16.0 follow-redirects: 1.16.0(debug@4.4.3)
form-data: 4.0.6 form-data: 4.0.6
https-proxy-agent: 5.0.1 https-proxy-agent: 5.0.1
proxy-from-env: 2.1.0 proxy-from-env: 2.1.0
@ -1710,7 +1737,9 @@ snapshots:
optionalDependencies: optionalDependencies:
picomatch: 4.0.5 picomatch: 4.0.5
follow-redirects@1.16.0: {} follow-redirects@1.16.0(debug@4.4.3):
optionalDependencies:
debug: 4.4.3
form-data@4.0.6: form-data@4.0.6:
dependencies: dependencies:
@ -2149,7 +2178,7 @@ snapshots:
vue-tsc@3.3.10(typescript@5.6.3): vue-tsc@3.3.10(typescript@5.6.3):
dependencies: dependencies:
'@volar/typescript': 2.4.28 '@volar/typescript': 2.4.28(typescript@5.6.3)
'@vue/language-core': 3.3.10 '@vue/language-core': 3.3.10
typescript: 5.6.3 typescript: 5.6.3

349
src-tauri/Cargo.lock generated
View File

@ -53,6 +53,15 @@ version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
dependencies = [
"derive_arbitrary",
]
[[package]] [[package]]
name = "async-broadcast" name = "async-broadcast"
version = "0.7.2" version = "0.7.2"
@ -825,6 +834,17 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]] [[package]]
name = "derive_more" name = "derive_more"
version = "2.1.1" version = "2.1.1"
@ -1153,6 +1173,16 @@ dependencies = [
"rustc_version", "rustc_version",
] ]
[[package]]
name = "filetime"
version = "0.2.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
dependencies = [
"cfg-if",
"libc",
]
[[package]] [[package]]
name = "find-msvc-tools" name = "find-msvc-tools"
version = "0.1.11" version = "0.1.11"
@ -1783,6 +1813,21 @@ dependencies = [
"want", "want",
] ]
[[package]]
name = "hyper-rustls"
version = "0.27.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
dependencies = [
"http",
"hyper",
"hyper-util",
"rustls",
"tokio",
"tokio-rustls",
"tower-service",
]
[[package]] [[package]]
name = "hyper-util" name = "hyper-util"
version = "0.1.20" version = "0.1.20"
@ -2105,6 +2150,36 @@ dependencies = [
"windows-sys 0.45.0", "windows-sys 0.45.0",
] ]
[[package]]
name = "jni"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
dependencies = [
"cfg-if",
"combine",
"jni-macros",
"jni-sys 0.4.1",
"log",
"simd_cesu8",
"thiserror 2.0.20",
"walkdir",
"windows-link 0.2.1",
]
[[package]]
name = "jni-macros"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
dependencies = [
"proc-macro2",
"quote",
"rustc_version",
"simd_cesu8",
"syn 2.0.119",
]
[[package]] [[package]]
name = "jni-sys" name = "jni-sys"
version = "0.3.1" version = "0.3.1"
@ -2315,6 +2390,12 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minisign-verify"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.9" version = "0.8.9"
@ -2580,6 +2661,18 @@ dependencies = [
"objc2-core-foundation", "objc2-core-foundation",
] ]
[[package]]
name = "objc2-osa-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
dependencies = [
"bitflags 2.13.1",
"objc2",
"objc2-app-kit",
"objc2-foundation",
]
[[package]] [[package]]
name = "objc2-quartz-core" name = "objc2-quartz-core"
version = "0.3.2" version = "0.3.2"
@ -2713,6 +2806,20 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "osakit"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
dependencies = [
"objc2",
"objc2-foundation",
"objc2-osa-kit",
"serde",
"serde_json",
"thiserror 2.0.20",
]
[[package]] [[package]]
name = "pango" name = "pango"
version = "0.18.3" version = "0.18.3"
@ -3156,15 +3263,20 @@ dependencies = [
"http-body", "http-body",
"http-body-util", "http-body-util",
"hyper", "hyper",
"hyper-rustls",
"hyper-util", "hyper-util",
"js-sys", "js-sys",
"log", "log",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
"serde", "serde",
"serde_json", "serde_json",
"sync_wrapper", "sync_wrapper",
"tokio", "tokio",
"tokio-rustls",
"tokio-util", "tokio-util",
"tower", "tower",
"tower-http", "tower-http",
@ -3200,6 +3312,20 @@ dependencies = [
"windows-sys 0.60.2", "windows-sys 0.60.2",
] ]
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.17",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "2.1.3" version = "2.1.3"
@ -3228,6 +3354,79 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "rustls"
version = "0.23.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
dependencies = [
"openssl-probe",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pki-types"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
dependencies = [
"zeroize",
]
[[package]]
name = "rustls-platform-verifier"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
dependencies = [
"core-foundation",
"core-foundation-sys",
"jni 0.22.4",
"log",
"once_cell",
"rustls",
"rustls-native-certs",
"rustls-platform-verifier-android",
"rustls-webpki",
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls-platform-verifier-android"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.103.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.23" version = "1.0.23"
@ -3575,6 +3774,22 @@ version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
[[package]]
name = "simd_cesu8"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
dependencies = [
"rustc_version",
"simdutf8",
]
[[package]]
name = "simdutf8"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]] [[package]]
name = "siphasher" name = "siphasher"
version = "1.0.3" version = "1.0.3"
@ -3861,7 +4076,9 @@ dependencies = [
"tauri-helper", "tauri-helper",
"tauri-plugin-dialog", "tauri-plugin-dialog",
"tauri-plugin-opener", "tauri-plugin-opener",
"tauri-plugin-process",
"tauri-plugin-store", "tauri-plugin-store",
"tauri-plugin-updater",
] ]
[[package]] [[package]]
@ -3905,6 +4122,12 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "swift-rs" name = "swift-rs"
version = "1.0.7" version = "1.0.7"
@ -3999,7 +4222,7 @@ dependencies = [
"gdkwayland-sys", "gdkwayland-sys",
"gdkx11-sys", "gdkx11-sys",
"gtk", "gtk",
"jni", "jni 0.21.1",
"libc", "libc",
"log", "log",
"ndk", "ndk",
@ -4032,6 +4255,17 @@ dependencies = [
"syn 2.0.119", "syn 2.0.119",
] ]
[[package]]
name = "tar"
version = "0.4.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
dependencies = [
"filetime",
"libc",
"xattr",
]
[[package]] [[package]]
name = "target-lexicon" name = "target-lexicon"
version = "0.12.16" version = "0.12.16"
@ -4055,7 +4289,7 @@ dependencies = [
"gtk", "gtk",
"heck 0.5.0", "heck 0.5.0",
"http", "http",
"jni", "jni 0.21.1",
"libc", "libc",
"log", "log",
"mime", "mime",
@ -4246,6 +4480,16 @@ dependencies = [
"zbus", "zbus",
] ]
[[package]]
name = "tauri-plugin-process"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a"
dependencies = [
"tauri",
"tauri-plugin",
]
[[package]] [[package]]
name = "tauri-plugin-store" name = "tauri-plugin-store"
version = "2.4.4" version = "2.4.4"
@ -4262,6 +4506,39 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "tauri-plugin-updater"
version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af"
dependencies = [
"base64 0.22.1",
"dirs",
"flate2",
"futures-util",
"http",
"infer",
"log",
"minisign-verify",
"osakit",
"percent-encoding",
"reqwest",
"rustls",
"semver",
"serde",
"serde_json",
"tar",
"tauri",
"tauri-plugin",
"tempfile",
"thiserror 2.0.20",
"time",
"tokio",
"url",
"windows-sys 0.60.2",
"zip",
]
[[package]] [[package]]
name = "tauri-runtime" name = "tauri-runtime"
version = "2.11.3" version = "2.11.3"
@ -4272,7 +4549,7 @@ dependencies = [
"dpi", "dpi",
"gtk", "gtk",
"http", "http",
"jni", "jni 0.21.1",
"objc2", "objc2",
"objc2-ui-kit", "objc2-ui-kit",
"objc2-web-kit", "objc2-web-kit",
@ -4295,7 +4572,7 @@ checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f"
dependencies = [ dependencies = [
"gtk", "gtk",
"http", "http",
"jni", "jni 0.21.1",
"log", "log",
"objc2", "objc2",
"objc2-app-kit", "objc2-app-kit",
@ -4532,6 +4809,16 @@ dependencies = [
"syn 3.0.3", "syn 3.0.3",
] ]
[[package]]
name = "tokio-rustls"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [
"rustls",
"tokio",
]
[[package]] [[package]]
name = "tokio-stream" name = "tokio-stream"
version = "0.1.19" version = "0.1.19"
@ -4878,6 +5165,12 @@ version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.8" version = "2.5.8"
@ -5127,6 +5420,15 @@ dependencies = [
"system-deps", "system-deps",
] ]
[[package]]
name = "webpki-root-certs"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
dependencies = [
"rustls-pki-types",
]
[[package]] [[package]]
name = "webview2-com" name = "webview2-com"
version = "0.38.2" version = "0.38.2"
@ -5363,6 +5665,15 @@ dependencies = [
"windows-targets 0.42.2", "windows-targets 0.42.2",
] ]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.59.0" version = "0.59.0"
@ -5658,7 +5969,7 @@ dependencies = [
"gtk", "gtk",
"http", "http",
"javascriptcore-rs", "javascriptcore-rs",
"jni", "jni 0.21.1",
"libc", "libc",
"ndk", "ndk",
"objc2", "objc2",
@ -5705,6 +6016,16 @@ dependencies = [
"pkg-config", "pkg-config",
] ]
[[package]]
name = "xattr"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
dependencies = [
"libc",
"rustix",
]
[[package]] [[package]]
name = "yoke" name = "yoke"
version = "0.8.3" version = "0.8.3"
@ -5819,6 +6140,12 @@ dependencies = [
"synstructure", "synstructure",
] ]
[[package]]
name = "zeroize"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
[[package]] [[package]]
name = "zerotrie" name = "zerotrie"
version = "0.2.5" version = "0.2.5"
@ -5852,6 +6179,18 @@ dependencies = [
"syn 3.0.3", "syn 3.0.3",
] ]
[[package]]
name = "zip"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
dependencies = [
"arbitrary",
"crc32fast",
"indexmap 2.14.0",
"memchr",
]
[[package]] [[package]]
name = "zmij" name = "zmij"
version = "1.0.23" version = "1.0.23"

View File

@ -39,3 +39,5 @@ tauri-plugin-dialog = "2.7.2"
encoding_rs = "0.8.35" encoding_rs = "0.8.35"
regex = "1.13.1" regex = "1.13.1"
chardetng = "1.0.0" chardetng = "1.0.0"
tauri-plugin-updater = "2.10.1"
tauri-plugin-process = "2.3.1"

View File

@ -13,6 +13,10 @@
"core:window:allow-set-size", "core:window:allow-set-size",
"opener:default", "opener:default",
"dialog:default", "dialog:default",
"dialog:allow-open" "dialog:allow-open",
"updater:default",
"updater:allow-check",
"process:default",
"process:allow-restart"
] ]
} }

View File

@ -1,19 +1,23 @@
mod commands; mod commands;
mod services;
mod models; mod models;
mod services;
use tauri::Manager;
use commands::book::*; use commands::book::*;
use services::db::init_pool; use services::db::init_pool;
use tauri::Manager;
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init()) // 对话框插件
.plugin(tauri_plugin_updater::Builder::new().build()) // 更新插件
.plugin(tauri_plugin_process::init()) // 进程插件(更新后重启)
.setup(|app| { .setup(|app| {
let pool = tauri::async_runtime::block_on(async { let pool = tauri::async_runtime::block_on(async {
init_pool(app.handle()).await.expect("sqlite数据库连接池初始化失败!") init_pool(app.handle())
.await
.expect("sqlite数据库连接池初始化失败!")
}); });
app.manage(pool); app.manage(pool);
Ok(()) Ok(())

View File

@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "stealthreader", "productName": "StealthReader",
"version": "0.2.0", "version": "../package.json",
"identifier": "StealthReader", "identifier": "StealthReader",
"build": { "build": {
"beforeDevCommand": "pnpm dev", "beforeDevCommand": "pnpm dev",
@ -23,7 +23,7 @@
"skipTaskbar": false, "skipTaskbar": false,
"shadow": false, "shadow": false,
"visible": true, "visible": true,
"center": false "center": false
} }
], ],
"security": { "security": {
@ -32,13 +32,16 @@
}, },
"bundle": { "bundle": {
"active": true, "active": true,
"createUpdaterArtifacts": true,
"targets": "all", "targets": "all",
"icon": [ "icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"]
"icons/32x32.png", },
"icons/128x128.png", "plugins": {
"icons/128x128@2x.png", "updater": {
"icons/icon.icns", "active": true,
"icons/icon.ico" "dialog": true,
] "endpoints": ["https://github.com/SparksFly91/StealthReader/releases/latest/download/latest.json"],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDYyQjM0MUIxMDNGRkMxOUQKUldTZHdmOERzVUd6WWx5TzJlczBjU2dWSXdzSjhwVjRtOFJZUDJ2ajB1MHdTS1p3NmhuZlIwNloK"
}
} }
} }

View File

@ -14,6 +14,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { zhCN, dateZhCN } from "naive-ui" import { zhCN, dateZhCN } from "naive-ui"
import GlobalLoading from "@/components/GlobalLoading.vue" import GlobalLoading from "@/components/GlobalLoading.vue"
import { checkAndUpdate } from "@/utils/updater"
onMounted(() => {
checkAndUpdate()
})
</script> </script>
<style scoped> <style scoped>

54
src/utils/updater.ts Normal file
View File

@ -0,0 +1,54 @@
import { check, type Update } from "@tauri-apps/plugin-updater"
import { ask } from "@tauri-apps/plugin-dialog"
import { relaunch } from "@tauri-apps/plugin-process"
/**
*
* - updated
* - none
* - cancelled
* - error
*/
export type CheckUpdateResult = "updated" | "none" | "cancelled" | "error"
/**
* Update null
*/
export async function checkUpdate(): Promise<Update | null> {
return await check()
}
/**
*
* @param update
* @returns true=false=
*/
export async function promptAndInstall(update: Update): Promise<boolean> {
const yes = await ask(
`发现新版本 ${update.version},是否现在更新?\n\n${update.body || ""}`,
{ title: "发现更新", kind: "info" }
)
if (!yes) return false
await update.downloadAndInstall()
await relaunch()
return true
}
/**
*
*
* @returns
*/
export async function checkAndUpdate(): Promise<CheckUpdateResult> {
try {
const update = await check()
if (!update) return "none"
const installed = await promptAndInstall(update)
return installed ? "updated" : "cancelled"
} catch (error) {
console.error("检查更新失败:", error)
return "error"
}
}

View File

@ -20,31 +20,44 @@
<n-descriptions-item label="总章节">{{ book?.total_chapters ?? 0 }}</n-descriptions-item> <n-descriptions-item label="总章节">{{ book?.total_chapters ?? 0 }}</n-descriptions-item>
<n-descriptions-item label="总字数">{{ formatChars(book?.total_chars) }}</n-descriptions-item> <n-descriptions-item label="总字数">{{ formatChars(book?.total_chars) }}</n-descriptions-item>
<n-descriptions-item label="导入时间">{{ formatTime(book?.create_time) }}</n-descriptions-item> <n-descriptions-item label="导入时间">{{ formatTime(book?.create_time) }}</n-descriptions-item>
<n-descriptions-item label="上次阅读">{{ formatTime(book?.last_read_time) }}</n-descriptions-item>
<n-descriptions-item label="阅读进度">{{ readProgress }}</n-descriptions-item>
<n-descriptions-item label="简介" :span="2">
<n-popover
trigger="hover"
placement="bottom-start"
:show-arrow="true"
content-class="intro-popover-panel"
:content-style="{
width: '520px',
maxWidth: 'calc(100vw - 64px)',
maxHeight: '180px',
padding: '0',
overflow: 'hidden',
}"
>
<template #trigger>
<div class="intro-preview">{{ book?.introduction || "暂无简介" }}</div>
</template>
<n-scrollbar class="intro-popover-scrollbar" style="height: 180px">
<div class="intro-popover">{{ book?.introduction || "暂无简介" }}</div>
</n-scrollbar>
</n-popover>
</n-descriptions-item>
</n-descriptions> </n-descriptions>
<div class="progress-row">
<n-button v-if="continueInfo" class="continue-btn" @click="continueRead">
<template #default>
<div class="continue-content">
<div class="continue-action">点击继续阅读</div>
<div class="continue-main">
<span class="continue-prefix">阅读到</span>
<span class="continue-title" :title="continueInfo.title">{{ continueTitle }}</span>
<span class="continue-percent">{{ continuePercent }}%</span>
</div>
<div class="continue-time">上次阅读 {{ formatTime(book?.last_read_time) }}</div>
</div>
</template>
</n-button>
<n-popover
class="intro-popover-wrap"
trigger="hover"
placement="bottom-start"
:show-arrow="true"
content-class="intro-popover-panel"
:content-style="{
width: '520px',
maxWidth: 'calc(100vw - 64px)',
maxHeight: '180px',
padding: '0',
overflow: 'hidden',
}"
>
<template #trigger>
<div class="intro-preview">{{ book?.introduction || "暂无简介" }}</div>
</template>
<n-scrollbar class="intro-popover-scrollbar" style="height: 180px">
<div class="intro-popover">{{ book?.introduction || "暂无简介" }}</div>
</n-scrollbar>
</n-popover>
</div>
</div> </div>
</div> </div>
</section> </section>
@ -137,6 +150,7 @@ const page = ref(1)
const limit = ref(50) const limit = ref(50)
const loadingChapters = ref(false) const loadingChapters = ref(false)
const keyword = ref("") const keyword = ref("")
const lastReadChapter = ref<Chapters | null>(null)
const goBack = () => router.back() const goBack = () => router.back()
@ -144,11 +158,30 @@ const readChapter = (ch: Chapters) => {
router.push({ name: "Reader", query: { id: ch.id } }) router.push({ name: "Reader", query: { id: ch.id } })
} }
const readProgress = computed(() => { const continueRead = () => {
const ch = lastReadChapter.value
if (ch) {
router.push({ name: "Reader", query: { id: ch.id } })
}
}
const continueInfo = computed(() => {
const b = book.value const b = book.value
if (!b || b.total_chapters <= 0) return "—" if (!b || b.last_read_chapter_id <= 0) return null
const percent = Math.round((b.last_read_chapter_id / b.total_chapters) * 100) return lastReadChapter.value
return `${b.last_read_chapter_id} 章 · ${percent}%` })
const continueTitle = computed(() => {
const t = lastReadChapter.value?.title
if (!t) return ""
return t.length > 5 ? `${t.slice(0, 5)}` : t
})
const continuePercent = computed(() => {
const b = book.value
const ch = lastReadChapter.value
if (!b || b.total_chapters <= 0 || !ch) return 0
return Math.round((ch.number / b.total_chapters) * 100)
}) })
const formatChars = (chars?: number) => { const formatChars = (chars?: number) => {
@ -168,11 +201,25 @@ const loadBook = async () => {
const res = await withLoading(() => BookApi.detail(bookId.value), "加载中...") const res = await withLoading(() => BookApi.detail(bookId.value), "加载中...")
if (res.code === 0) { if (res.code === 0) {
book.value = res.data book.value = res.data
if (res.data.last_read_chapter_id > 0) {
await loadLastReadChapter(res.data.last_read_chapter_id)
}
} else { } else {
message.error(res.msg) message.error(res.msg)
} }
} }
const loadLastReadChapter = async (chapterId: number) => {
try {
const res = await BookApi.chapterDetail(chapterId)
if (res.code === 0) {
lastReadChapter.value = res.data
}
} catch {
lastReadChapter.value = null
}
}
const loadChapters = async () => { const loadChapters = async () => {
if (!bookId.value) return if (!bookId.value) return
loadingChapters.value = true loadingChapters.value = true
@ -277,17 +324,99 @@ onMounted(() => {
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.intro-popover-wrap {
flex: 1;
min-width: 0;
}
.intro-preview { .intro-preview {
box-sizing: border-box;
height: var(--progress-height);
min-width: 0;
line-height: 1.6; line-height: 1.6;
font-size: 13px; font-size: 13px;
color: var(--color-text-secondary); color: var(--color-text-secondary);
word-break: break-all; word-break: break-all;
white-space: pre-wrap; white-space: pre-wrap;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden; overflow: hidden;
cursor: help; cursor: help;
padding: 10px 0;
border-radius: var(--radius-sm);
}
.progress-row {
--progress-height: 84px;
display: flex;
align-items: flex-start;
gap: 16px;
}
.continue-btn {
box-sizing: border-box;
flex-shrink: 0;
width: 220px;
height: var(--progress-height);
border-radius: var(--radius-card);
padding: 10px 16px;
text-align: left;
transition: box-shadow 0.2s ease, background-color 0.2s ease;
&:hover {
box-shadow: var(--shadow-card-hover);
}
:deep(.n-button__content) {
width: 100%;
height: 100%;
}
}
.continue-content {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
gap: 4px;
width: 100%;
height: 100%;
}
.continue-action {
font-size: 14px;
font-weight: 600;
line-height: 1.4;
color: var(--color-text-primary);
}
.continue-main {
display: flex;
align-items: baseline;
gap: 6px;
font-size: 13px;
line-height: 1.4;
}
.continue-prefix {
color: var(--color-text-secondary);
}
.continue-title {
max-width: 5em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--color-text-primary);
}
.continue-percent {
font-weight: 700;
color: var(--color-accent);
}
.continue-time {
font-size: 12px;
line-height: 1.4;
color: var(--color-text-secondary);
} }
:global(.intro-popover-scrollbar .n-scrollbar-container) { :global(.intro-popover-scrollbar .n-scrollbar-container) {

View File

@ -1,7 +1,9 @@
<template> <template>
<div class="glass-panel" :style="readerStyle" data-tauri-drag-region @mousedown="onWindowMouseDown"> <div class="glass-panel" :style="readerStyle" data-tauri-drag-region @mousedown="onWindowMouseDown">
<div class="reading-wrap"> <div class="reading-wrap">
<div v-if="currentPage == 0" class="chapter-name">{{ chapter?.title || "加载中..." }}</div> <div class="chapter-name" :class="{ 'chapter-name--hidden': currentPage !== 0 }">
{{ 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>
@ -252,6 +254,10 @@ onUnmounted(() => {
margin-bottom: 20px; margin-bottom: 20px;
} }
.chapter-name--hidden {
visibility: hidden;
}
.page-body { .page-body {
flex: 1; flex: 1;
min-height: 0; min-height: 0;

View File

@ -1,18 +1,156 @@
<template> <template>
<div class="setting-page"> <div class="setting-page">
<h2 class="page-title">关于</h2> <h2 class="page-title">关于</h2>
<p class="page-desc">关于本应用的信息</p> <p class="page-desc">关于幽灵阅读器</p>
<div class="about-card"> <div class="about-hero">
<div class="app-logo"></div> <div class="about-logo">
<div class="app-name">幽灵阅读器</div> <img :src="appIcon" alt="幽灵阅读器" />
<div class="app-version">版本 0.1.0</div> </div>
<div class="app-desc">一款安静沉浸的本地阅读器</div> <div class="about-info">
<div class="about-name">幽灵阅读器</div>
<div class="about-subname">Stealth Reader</div>
<div class="about-meta">
<div class="about-version">版本 {{ version || "0.0.0" }}</div>
<n-button
class="check-update-btn"
size="tiny"
secondary
round
:loading="checkingUpdate"
@click="onCheckUpdate"
>
<template #icon>
<n-icon :component="SyncOutlined" />
</template>
{{ checkingUpdate ? "检查中..." : "检查更新" }}
</n-button>
</div>
</div>
</div>
<div class="about-intro">
一款安静本地优先的桌面小说阅读器轻量纯本地无账号无广告专注好好读一本书
</div>
<div class="about-section">
<div class="section-title">核心特性</div>
<div class="feature-list">
<div v-for="f in features" :key="f.title" class="feature-item">
<span class="feature-icon" :style="{ backgroundColor: f.bg }">
<n-icon :component="f.icon" :size="16" />
</span>
<div class="feature-body">
<div class="feature-title">{{ f.title }}</div>
<div class="feature-desc">{{ f.desc }}</div>
</div>
</div>
</div>
</div>
<div class="about-section">
<div class="section-title">技术栈</div>
<div class="tech-list">
<div v-for="t in tech" :key="t.name" class="tech-item">
<span class="tech-name">{{ t.name }}</span>
<span class="tech-desc">{{ t.desc }}</span>
</div>
</div>
</div>
<div class="about-footer">
<span>© {{ year }} Stealth Reader</span>
<span>Apache-2.0 License</span>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"></script> <script setup lang="ts">
import { getVersion } from "@tauri-apps/api/app"
import {
BookOutlined,
FileTextOutlined,
FontColorsOutlined,
LockOutlined,
ScanOutlined,
SyncOutlined,
ThunderboltOutlined,
} from "@vicons/antd"
import appIcon from "../../../app-icon.svg"
import { checkAndUpdate } from "@/utils/updater"
const message = useMessage()
const year = new Date().getFullYear()
const version = ref("")
const checkingUpdate = ref(false)
getVersion().then((v) => {
version.value = v
})
const onCheckUpdate = async () => {
if (checkingUpdate.value) return
checkingUpdate.value = true
try {
const result = await checkAndUpdate()
if (result === "none") {
message.success("当前已是最新版本")
} else if (result === "error") {
message.error("检查更新失败,请稍后重试")
}
// updatedcancelled
} finally {
checkingUpdate.value = false
}
}
const features = [
{
title: "本地优先",
desc: "纯本地存储,数据不离开你的设备",
icon: LockOutlined,
bg: "color-mix(in srgb, #667eea 14%, transparent)",
},
{
title: "智能分章",
desc: "自动识别「第X章」「Chapter N」等章节格式",
icon: ScanOutlined,
bg: "color-mix(in srgb, #10b981 14%, transparent)",
},
{
title: "自动编码检测",
desc: "GBK / UTF-8 等中文编码自动识别",
icon: FileTextOutlined,
bg: "color-mix(in srgb, #f59e0b 14%, transparent)",
},
{
title: "沉浸阅读",
desc: "可拖拽窗口、透明背景、翻页阅读",
icon: BookOutlined,
bg: "color-mix(in srgb, #ec4899 14%, transparent)",
},
{
title: "外观自定义",
desc: "字体大小 / 颜色 / 背景随心调整",
icon: FontColorsOutlined,
bg: "color-mix(in srgb, #8b5cf6 14%, transparent)",
},
{
title: "轻量快速",
desc: "Tauri 2 构建,内存占用远低于 Electron",
icon: ThunderboltOutlined,
bg: "color-mix(in srgb, #06b6d4 14%, transparent)",
},
]
const tech = [
{ name: "桌面框架", desc: "Tauri 2 + Rust" },
{ name: "前端框架", desc: "Vue 3 + TypeScript" },
{ name: "UI 组件", desc: "Naive UI" },
{ name: "数据存储", desc: "SQLite (sqlx)" },
{ name: "编码检测", desc: "chardetng" },
]
</script>
<style lang="scss" scoped> <style lang="scss" scoped>
.setting-page { .setting-page {
@ -33,48 +171,185 @@
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
.about-card { .about-hero {
display: grid; display: flex;
grid-template-columns: 52px minmax(0, 1fr);
column-gap: 14px;
align-items: center; align-items: center;
min-height: 108px; gap: 16px;
padding: 18px 16px; padding: 20px 16px;
border-top: 1px solid var(--color-border); border-top: 1px solid var(--color-border);
border-bottom: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border);
} }
.app-logo { .about-logo {
width: 52px; width: 64px;
height: 52px; height: 64px;
flex-shrink: 0;
border-radius: 16px;
overflow: hidden;
box-shadow: var(--shadow-card);
img {
width: 100%;
height: 100%;
display: block;
}
}
.about-info {
min-width: 0;
}
.about-name {
font-size: 18px;
font-weight: 700;
color: var(--color-text-primary);
}
.about-subname {
font-size: 13px;
color: var(--color-text-secondary);
margin-top: 1px;
}
.about-version {
display: inline-block;
margin-top: 6px;
padding: 1px 8px;
border-radius: 999px;
background-color: color-mix(in srgb, var(--color-accent) 12%, transparent);
color: var(--color-accent);
font-size: 12px;
font-weight: 600;
}
.about-meta {
display: flex;
align-items: center;
gap: 10px;
}
.check-update-btn {
margin-top: 6px;
font-weight: 500;
:deep(.n-button__icon) {
margin-right: 4px;
}
}
.about-intro {
margin: 18px 0;
padding: 14px 16px;
border-radius: var(--radius-sm);
background-color: var(--color-window-bg);
font-size: 13px;
line-height: 1.7;
color: var(--color-text-secondary);
}
.about-section {
& + & {
margin-top: 20px;
}
}
.section-title {
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: 10px;
}
.feature-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.feature-item {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 12px;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
&:hover {
border-color: var(--color-border-hover);
box-shadow: var(--shadow-card);
}
}
.feature-icon {
width: 32px;
height: 32px;
flex-shrink: 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 8px; border-radius: 8px;
background: var(--color-accent-gradient); color: var(--color-text-primary);
color: var(--color-text-invert);
font-size: 22px;
font-weight: 700;
grid-row: span 2;
} }
.app-name { .feature-body {
font-size: 16px; min-width: 0;
}
.feature-title {
font-size: 13px;
font-weight: 600; font-weight: 600;
color: var(--color-text-primary); color: var(--color-text-primary);
} }
.app-version { .feature-desc {
font-size: 13px; margin-top: 2px;
font-size: 12px;
line-height: 1.5;
color: var(--color-text-secondary); color: var(--color-text-secondary);
margin: 2px 0 0;
} }
.app-desc { .tech-list {
grid-column: 1 / -1; overflow: hidden;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
}
.tech-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 14px;
min-height: 40px;
& + & {
border-top: 1px solid var(--color-border);
}
}
.tech-name {
font-size: 13px;
font-weight: 600;
color: var(--color-text-primary);
}
.tech-desc {
font-size: 13px; font-size: 13px;
color: var(--color-text-secondary); color: var(--color-text-secondary);
margin: 12px 0 0; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
.about-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 24px;
padding-top: 14px;
border-top: 1px solid var(--color-border);
font-size: 12px;
color: var(--color-text-secondary);
} }
@media (max-width: 640px) { @media (max-width: 640px) {
@ -82,8 +357,14 @@
padding: 18px 16px 24px; padding: 18px 16px 24px;
} }
.app-desc { .feature-list {
margin-left: 0; grid-template-columns: 1fr;
}
.about-footer {
flex-direction: column;
align-items: flex-start;
gap: 4px;
} }
} }
</style> </style>