Skip to content

Commit 335373d

Browse files
authored
Merge pull request #48 from shadowfish07/issue-32
feat: 新增“翻译设置”页面,支持用户自定义翻译服务提供商、目标语言及缓存开关,并可一键清除翻译缓存
2 parents 959862d + 1380986 commit 335373d

File tree

14 files changed

+858
-122
lines changed

14 files changed

+858
-122
lines changed

.releaserc.json

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,6 @@
4949
}
5050
}
5151
],
52-
[
53-
"@semantic-release/changelog",
54-
{
55-
"changelogFile": "CHANGELOG.md"
56-
}
57-
],
5852
[
5953
"@semantic-release/exec",
6054
{
@@ -87,4 +81,4 @@
8781
}
8882
]
8983
]
90-
}
84+
}

.trae/rules/project_rules.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
这个 APP 可以连接 Readeck 的数据,作为其手机端程序。
44

5-
完成代码变动后,你应当始终执行 `flutter analyze` 命令,进行代码检查和错误修复。
5+
完成代码变动后,你应当始终执行 `flutter analyze` 命令,进行代码检查和错误修复。但始终不要执行 `flutter run` 命令。
66

77
所有样式都应使用主题,不允许存在硬编码颜色、字号等。
88

CHANGELOG.md

Lines changed: 0 additions & 78 deletions
This file was deleted.

lib/config/dependencies.dart

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,20 @@ List<SingleChildWidget> providers(String host, String token) {
2828
),
2929
),
3030
Provider(create: (context) => ReadeckApiClient(host, token)),
31+
Provider(create: (context) => DatabaseService()),
3132
Provider(
3233
create: (context) =>
33-
OpenRouterApiClient(context.read<SharedPreferencesService>())),
34-
Provider(create: (context) => DatabaseService()),
34+
SettingsRepository(context.read(), context.read(), context.read())),
3535
Provider(
3636
create: (context) =>
37-
BookmarkRepository(context.read(), context.read(), context.read())),
37+
OpenRouterApiClient(context.read<SharedPreferencesService>())),
38+
Provider(
39+
create: (context) => BookmarkRepository(
40+
context.read(), context.read(), context.read(), context.read())),
3841
Provider(create: (context) => DailyReadHistoryRepository(context.read())),
3942
Provider(create: (context) => LabelRepository(context.read())),
4043
Provider(
4144
create: (context) =>
4245
BookmarkOperationUseCases(context.read(), context.read())),
43-
Provider(
44-
create: (context) => SettingsRepository(context.read(), context.read()))
4546
];
4647
}

lib/data/repository/bookmark/bookmark_repository.dart

Lines changed: 57 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'dart:math';
22

3+
import 'package:readeck_app/data/repository/settings/settings_repository.dart';
34
import 'package:readeck_app/data/service/database_service.dart';
45
import 'package:readeck_app/data/service/openrouter_api_client.dart';
56
import 'package:readeck_app/data/service/readeck_api_client.dart';
@@ -12,12 +13,13 @@ import 'package:result_dart/result_dart.dart';
1213
typedef BookmarkChangeListener = void Function();
1314

1415
class BookmarkRepository {
15-
BookmarkRepository(
16-
this._readeckApiClient, this._databaseService, this._openRouterApiClient);
16+
BookmarkRepository(this._readeckApiClient, this._databaseService,
17+
this._openRouterApiClient, this._settingsRepository);
1718

1819
final ReadeckApiClient _readeckApiClient;
1920
final DatabaseService _databaseService;
2021
final OpenRouterApiClient _openRouterApiClient;
22+
final SettingsRepository _settingsRepository;
2123

2224
// 全局共享数据管理 - 单一数据源
2325
final List<Bookmark> _bookmarks = [];
@@ -330,43 +332,54 @@ class BookmarkRepository {
330332
}
331333

332334
/// 翻译书签内容(流式输出)
333-
/// 优先从缓存获取翻译,如果缓存没有则使用AI翻译并写入缓存
335+
/// 根据缓存配置决定是否使用缓存,如果启用缓存则优先从缓存获取翻译
334336
Stream<Result<String>> translateBookmarkContentStream(
335337
String bookmarkId, String originalContent) async* {
336338
try {
337339
appLogger.i('开始翻译书签内容: $bookmarkId');
338340

339-
// 首先尝试从缓存获取翻译
340-
final cachedResult =
341-
await _databaseService.getBookmarkArticleByBookmarkId(bookmarkId);
342-
343-
if (cachedResult.isSuccess()) {
344-
final cachedArticle = cachedResult.getOrNull()!;
345-
if (cachedArticle.translate != null &&
346-
cachedArticle.translate!.isNotEmpty) {
347-
appLogger.i('从缓存获取翻译内容成功: $bookmarkId');
348-
yield Success(cachedArticle.translate!);
349-
return;
341+
// 获取翻译缓存配置
342+
final cacheEnabledResult =
343+
await _settingsRepository.getTranslationCacheEnabled();
344+
final isCacheEnabled = cacheEnabledResult.getOrDefault(true); // 默认启用缓存
345+
346+
// 获取翻译目标语言
347+
final targetLanguageResult =
348+
await _settingsRepository.getTranslationTargetLanguage();
349+
final targetLanguage = targetLanguageResult.getOrDefault('中文'); // 默认中文
350+
351+
appLogger.d('翻译缓存配置: ${isCacheEnabled ? "启用" : "禁用"}');
352+
appLogger.d('翻译目标语言: $targetLanguage');
353+
354+
// 如果启用缓存,首先尝试从缓存获取翻译
355+
if (isCacheEnabled) {
356+
final cachedResult =
357+
await _databaseService.getBookmarkArticleByBookmarkId(bookmarkId);
358+
359+
if (cachedResult.isSuccess()) {
360+
final cachedArticle = cachedResult.getOrNull()!;
361+
if (cachedArticle.translate != null &&
362+
cachedArticle.translate!.isNotEmpty) {
363+
appLogger.i('从缓存获取翻译内容成功: $bookmarkId');
364+
yield Success(cachedArticle.translate!);
365+
return;
366+
}
350367
}
368+
} else {
369+
appLogger.i('翻译缓存已禁用,直接使用AI翻译: $bookmarkId');
351370
}
352371

353372
// 缓存中没有翻译,使用AI进行翻译
354373
appLogger.i('缓存中未找到翻译,使用AI翻译: $bookmarkId');
355374

356-
// 构建翻译提示
357-
final messages = [
358-
{
359-
'role': 'system',
360-
'content':
361-
'你是一个专业的翻译助手。请将用户提供的HTML内容翻译成中文,保持HTML标签结构不变,只翻译文本内容。请确保翻译准确、流畅、符合中文表达习惯。'
362-
},
363-
{'role': 'user', 'content': originalContent}
364-
];
365-
366-
// 使用流式API进行翻译
367-
final translationStream = _openRouterApiClient.streamChatCompletion(
375+
// 根据目标语言构建翻译提示
376+
final translationPrompt =
377+
_buildTranslationPrompt(targetLanguage, originalContent);
378+
379+
// 使用流式Completion API进行翻译
380+
final translationStream = _openRouterApiClient.streamCompletion(
368381
model: 'google/gemini-2.5-flash',
369-
messages: messages,
382+
prompt: translationPrompt,
370383
temperature: 0.3,
371384
);
372385

@@ -387,9 +400,13 @@ class BookmarkRepository {
387400
}
388401
}
389402

390-
// 将翻译结果保存到缓存
391-
await _saveTranslationToCache(
392-
bookmarkId, originalContent, translatedContent);
403+
// 如果启用缓存,将翻译结果保存到缓存
404+
if (isCacheEnabled) {
405+
await _saveTranslationToCache(
406+
bookmarkId, originalContent, translatedContent);
407+
} else {
408+
appLogger.d('翻译缓存已禁用,不保存翻译结果: $bookmarkId');
409+
}
393410

394411
appLogger.i('AI翻译完成: $bookmarkId');
395412
} catch (e) {
@@ -398,6 +415,16 @@ class BookmarkRepository {
398415
}
399416
}
400417

418+
/// 根据目标语言构建翻译提示
419+
String _buildTranslationPrompt(String targetLanguage, String content) {
420+
return '''You are a professional translation assistant. Please translate the following HTML content into $targetLanguage, keeping the HTML tag structure unchanged and only translating the text content. Ensure the translation is accurate, fluent, and follows the expression habits of $targetLanguage.
421+
422+
Content to translate:
423+
$content
424+
425+
Translated content:''';
426+
}
427+
401428
/// 将翻译结果保存到缓存
402429
Future<void> _saveTranslationToCache(String bookmarkId,
403430
String originalContent, String translatedContent) async {

lib/data/repository/settings/settings_repository.dart

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import 'package:readeck_app/data/service/readeck_api_client.dart';
2+
import 'package:readeck_app/data/service/database_service.dart';
23
import 'package:readeck_app/data/service/shared_preference_service.dart';
34
import 'package:readeck_app/main.dart';
45
import 'package:result_dart/result_dart.dart';
56

67
class SettingsRepository {
7-
SettingsRepository(this._apiClient, this._prefsService);
8+
SettingsRepository(
9+
this._apiClient, this._prefsService, this._databaseService);
810

911
final ReadeckApiClient _apiClient;
1012
final SharedPreferencesService _prefsService;
13+
final DatabaseService _databaseService;
1114

1215
AsyncResult<bool> isApiConfigured() async {
1316
if (await _prefsService.getReadeckApiHost().getOrDefault('') == '') {
@@ -74,4 +77,74 @@ class SettingsRepository {
7477

7578
return Success(result.getOrThrow());
7679
}
80+
81+
/// 保存翻译服务提供方
82+
AsyncResult<void> saveTranslationProvider(String provider) async {
83+
final result = await _prefsService.setTranslationProvider(provider);
84+
if (result.isError()) {
85+
appLogger.e("保存翻译服务提供方失败", error: result.exceptionOrNull());
86+
return result;
87+
}
88+
return const Success(unit);
89+
}
90+
91+
/// 获取翻译服务提供方
92+
AsyncResult<String> getTranslationProvider() async {
93+
final result = await _prefsService.getTranslationProvider();
94+
if (result.isError()) {
95+
appLogger.e("获取翻译服务提供方失败", error: result.exceptionOrNull());
96+
return Failure(Exception(result.exceptionOrNull()));
97+
}
98+
return Success(result.getOrThrow());
99+
}
100+
101+
/// 保存翻译目标语种
102+
AsyncResult<void> saveTranslationTargetLanguage(String language) async {
103+
final result = await _prefsService.setTranslationTargetLanguage(language);
104+
if (result.isError()) {
105+
appLogger.e("保存翻译目标语种失败", error: result.exceptionOrNull());
106+
return result;
107+
}
108+
return const Success(unit);
109+
}
110+
111+
/// 获取翻译目标语种
112+
AsyncResult<String> getTranslationTargetLanguage() async {
113+
final result = await _prefsService.getTranslationTargetLanguage();
114+
if (result.isError()) {
115+
appLogger.e("获取翻译目标语种失败", error: result.exceptionOrNull());
116+
return Failure(Exception(result.exceptionOrNull()));
117+
}
118+
return Success(result.getOrThrow());
119+
}
120+
121+
/// 保存翻译缓存启用状态
122+
AsyncResult<void> saveTranslationCacheEnabled(bool enabled) async {
123+
final result = await _prefsService.setTranslationCacheEnabled(enabled);
124+
if (result.isError()) {
125+
appLogger.e("保存翻译缓存启用状态失败", error: result.exceptionOrNull());
126+
return result;
127+
}
128+
return const Success(unit);
129+
}
130+
131+
/// 获取翻译缓存启用状态
132+
AsyncResult<bool> getTranslationCacheEnabled() async {
133+
final result = await _prefsService.getTranslationCacheEnabled();
134+
if (result.isError()) {
135+
appLogger.e("获取翻译缓存启用状态失败", error: result.exceptionOrNull());
136+
return Failure(Exception(result.exceptionOrNull()));
137+
}
138+
return Success(result.getOrThrow());
139+
}
140+
141+
/// 清空所有翻译缓存
142+
AsyncResult<void> clearTranslationCache() async {
143+
final result = await _databaseService.clearAllTranslationCache();
144+
if (result.isError()) {
145+
appLogger.e("清空翻译缓存失败", error: result.exceptionOrNull());
146+
return result;
147+
}
148+
return const Success(unit);
149+
}
77150
}

lib/data/service/database_service.dart

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,29 @@ class DatabaseService {
282282
}
283283
}
284284

285+
/// 清空所有翻译缓存
286+
AsyncResult<void> clearAllTranslationCache() async {
287+
if (_database == null) {
288+
return Failure(Exception("Database is not open"));
289+
}
290+
291+
try {
292+
// 将所有书签文章的翻译字段设为 null
293+
final count = await _database!.update(
294+
_kTableBookmarkArticle,
295+
{_kColumnTranslate: null},
296+
);
297+
appLogger.i("Cleared translation cache for $count articles");
298+
return const Success(unit);
299+
} on Exception catch (e) {
300+
appLogger.e("Failed to clear translation cache", error: e);
301+
return Failure(e);
302+
} catch (e) {
303+
appLogger.e("Failed to clear translation cache", error: e);
304+
return Failure(Exception(e));
305+
}
306+
}
307+
285308
/// 清空所有数据库表的数据
286309
AsyncResult<void> clearAllData() async {
287310
if (_database == null) {

0 commit comments

Comments
 (0)