Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/build-arabic.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Build Arabic APK

on:
workflow_dispatch:
push:
branches: [arabic]

permissions:
contents: read

concurrency:
group: apk-${{ github.ref }}
cancel-in-progress: true

env:
FLUTTER_VERSION: 3.44.8

jobs:
apk:
name: Arabic release APK
runs-on: ubuntu-latest
timeout-minutes: 30
env:
GRADLE_OPTS: -Dorg.gradle.caching=true -Dorg.gradle.parallel=true
steps:
- uses: actions/checkout@v6
- name: Set up Gradle cache
uses: gradle/actions/setup-gradle@v5
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
pub-cache: true
- run: flutter pub get
- run: flutter analyze lib
- run: flutter build apk --release
- name: Upload Arabic APK
uses: actions/upload-artifact@v7
with:
name: tomoread-arabic-release-apk
path: build/app/outputs/flutter-apk/app-release.apk
if-no-files-found: error
retention-days: 30
24 changes: 12 additions & 12 deletions lib/app/appearance.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ enum ThemeSeed { blue, teal, green, orange, brown, purple }

extension ThemeSeedX on ThemeSeed {
String get label => switch (this) {
ThemeSeed.blue => '蓝色',
ThemeSeed.teal => '青色',
ThemeSeed.green => '绿色',
ThemeSeed.orange => '橙色',
ThemeSeed.brown => '棕色',
ThemeSeed.purple => '紫色',
ThemeSeed.blue => 'أزرق',
ThemeSeed.teal => 'أزرق',
ThemeSeed.green => 'أخضر',
ThemeSeed.orange => 'برتقالي',
ThemeSeed.brown => 'ألوان بُنّية',
ThemeSeed.purple => 'أرجوانيات',
};

Color get color => switch (this) {
Expand All @@ -33,15 +33,15 @@ enum AppThemeStyle { mist, white, paper }

extension AppThemeStyleX on AppThemeStyle {
String get label => switch (this) {
AppThemeStyle.mist => '雾绿',
AppThemeStyle.white => '纯白',
AppThemeStyle.paper => '纸张',
AppThemeStyle.mist => 'أخضر ضبابي',
AppThemeStyle.white => 'أبيض صلب',
AppThemeStyle.paper => 'الورق',
};

String get description => switch (this) {
AppThemeStyle.mist => '柔和的绿灰底色,适合日常使用。',
AppThemeStyle.white => '清晰的中性白色界面。',
AppThemeStyle.paper => '暖米色纸张底色,减少长时间阅读的刺眼感。',
AppThemeStyle.mist => 'خلفية خضراء رمادية ناعمة للاستخدام اليومي.',
AppThemeStyle.white => 'واجهة بيضاء محايدة واضحة.',
AppThemeStyle.paper => 'خلفية من الورق البيج الدافئ تقلل من وهج القراءات الطويلة.',
};

Color get previewColor => switch (this) {
Expand Down
6 changes: 3 additions & 3 deletions lib/app/providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -725,7 +725,7 @@ final textBookDocumentProvider = FutureProvider.autoDispose
final profile = results[1] as TextContentProfile?;
final chapters = results[2]! as List<TextChapter>;
if (book == null || profile == null) {
throw const TextDecodeException('文本书籍或编码配置不存在。');
throw const TextDecodeException('تكوين الكتاب المدرسي أو الترميز غير موجود.');
}
final decoded = await ref
.read(textDecoderServiceProvider)
Expand Down Expand Up @@ -753,7 +753,7 @@ final readerChapterProvider = FutureProvider.autoDispose
readerManifestProvider(request.bookId).future,
);
if (book == null || manifest == null) {
throw const EpubContentException('书籍或阅读清单不存在');
throw const EpubContentException('الكتاب أو قائمة القراءة غير موجودة');
}
return ref
.read(epubContentServiceProvider)
Expand All @@ -768,7 +768,7 @@ final epubExtractedDirectoryProvider = FutureProvider.autoDispose
.family<String, String>((ref, bookId) async {
final book = await ref.watch(readerBookProvider(bookId).future);
if (book == null) {
throw const EpubExtractionException('书籍不存在');
throw const EpubExtractionException('الكتاب غير موجود');
}
return ref.read(epubExtractionServiceProvider).ensureExtracted(book);
});
Expand Down
14 changes: 13 additions & 1 deletion lib/app/tomo_read_app.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

import '../data/database/app_database.dart';
Expand Down Expand Up @@ -50,6 +51,17 @@ class _TomoReadRoot extends ConsumerWidget {
theme: theme,
darkTheme: darkTheme,
themeMode: appearance.mode,
locale: const Locale('ar'),
supportedLocales: const [
Locale('ar'),
Locale('en'),
Locale('zh'),
],
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
builder: (context, child) => MediaQuery(
data: MediaQuery.of(
context,
Expand All @@ -64,7 +76,7 @@ class _TomoReadRoot extends ConsumerWidget {
child: FilledButton.icon(
onPressed: () => ref.invalidate(appSettingsProvider),
icon: const Icon(Icons.refresh),
label: const Text('重新加载设置'),
label: const Text('إعادة تحميل الإعدادات'),
),
),
),
Expand Down
18 changes: 9 additions & 9 deletions lib/features/chat/ai_agent_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ class AiAgentRunner {
AiProviderMessage(
role: 'system',
content:
'用户已显式选择“${declaration.displayName}”技能。本轮回答必须遵循以下技能结果,不必再次调用该技能:\n${result.output}',
'حدد المستخدم صراحة "${declaration.displayName}. يجب اتباع نتائج المهارة التالية لهذه الجولة من الردود دون الحاجة إلى استدعاء المهارة مرة أخرى:\n${result.output}',
),
);
} on Object catch (error) {
Expand Down Expand Up @@ -198,12 +198,12 @@ class AiAgentRunner {
AiCitationEvent(ordinal: ordinal, source: source),
);
sourceLines.add(
'[$ordinal] ${source.chapterTitle ?? '书中原文'}:${source.quote}',
'[$ordinal] ${source.chapterTitle ?? 'النص الأصلي في الكتاب'}:${source.quote}',
);
}
output = sourceLines.isEmpty
? result.output
: '${result.output}\n\n可引用来源:\n${sourceLines.join('\n')}';
: '${result.output}\n\nالمصدر (المصادر) التي يجب الاستشهاد بها:\n${sourceLines.join('\n')}';
stopwatch.stop();
controller.add(
AiToolExecutionCompletedEvent(
Expand All @@ -214,7 +214,7 @@ class AiAgentRunner {
);
} on Object catch (error) {
stopwatch.stop();
output = '工具执行失败:$error';
output = 'فشل تنفيذ الأداة: $error';
controller.add(
AiToolExecutionFailedEvent(
callId: call.id,
Expand All @@ -237,7 +237,7 @@ class AiAgentRunner {
} else {
throw const AiGatewayException(
'agent_iteration_limit',
'工具调用次数过多,已停止本次运行。',
'عدد كبير جدًا من مكالمات الأدوات، تم إيقاف هذا التشغيل.',
);
}
} on Object catch (error, stackTrace) {
Expand Down Expand Up @@ -306,9 +306,9 @@ class AiAgentRunner {
buffer
..writeln()
..writeln()
..writeln('[引用 ${index + 1}]')
..writeln('书籍:${quote.bookTitle}')
..writeln('章节:${quote.chapterTitle ?? '未知章节'}')
..writeln('المراجع${index + 1}]')
..writeln('الكتب:${quote.bookTitle}')
..writeln('الفصل${quote.chapterTitle ?? 'مقطع غير معروف'}')
..write(quote.quote);
}
return [AiProviderMessage(role: 'user', content: buffer.toString())];
Expand Down Expand Up @@ -369,7 +369,7 @@ class AiAgentRunner {
}

String _limitToolOutput(String value) =>
value.length <= 32768 ? value : '${value.substring(0, 32768)}\n[结果已截断]';
value.length <= 32768 ? value : '${value.substring(0, 32768)}\n[تم اقتطاع النتائج]';

void _appendContinuation(
List<AiProviderMessage> conversation, {
Expand Down
48 changes: 24 additions & 24 deletions lib/features/chat/ai_provider_configuration_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Future<void> configureAiProvider(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setState) => AlertDialog(
title: const Text('AI 服务商与模型'),
title: const Text('مقدمو ونماذج خدمات الذكاء الاصطناعي'),
content: SizedBox(
width: 560,
child: SingleChildScrollView(
Expand All @@ -41,17 +41,17 @@ Future<void> configureAiProvider(
children: [
DropdownButtonFormField<String?>(
initialValue: selectedProfileId,
decoration: const InputDecoration(labelText: '配置方案'),
decoration: const InputDecoration(labelText: 'تكوين السيناريوهات'),
items: [
const DropdownMenuItem<String?>(
value: null,
child: Text('新建配置'),
child: Text('تكوين جديد'),
),
...profiles.map(
(item) => DropdownMenuItem<String?>(
value: item.id,
child: Text(
'${item.name}${item.isActive ? '(当前)' : ''}',
'${item.name}${item.isActive ? 'الوضع الراهن' : ''}',
),
),
),
Expand Down Expand Up @@ -82,7 +82,7 @@ Future<void> configureAiProvider(
DropdownButtonFormField<String>(
key: ValueKey('provider-preset-$selectedPresetId'),
initialValue: selectedPresetId,
decoration: const InputDecoration(labelText: '服务商预设'),
decoration: const InputDecoration(labelText: 'الإعدادات المسبقة لمقدم الخدمة'),
items: AiProviderCatalog.presets
.where((item) => !item.deprecated)
.map(
Expand Down Expand Up @@ -118,7 +118,7 @@ Future<void> configureAiProvider(
const SizedBox(height: 12),
TextField(
controller: name,
decoration: const InputDecoration(labelText: '名称'),
decoration: const InputDecoration(labelText: 'الاسم'),
),
const SizedBox(height: 12),
TextField(
Expand All @@ -129,8 +129,8 @@ Future<void> configureAiProvider(
TextField(
controller: model,
decoration: const InputDecoration(
labelText: '模型',
hintText: '可手动输入,或从服务拉取',
labelText: 'النموذج',
hintText: 'يمكن إدخالها يدويًا أو سحبها من الخدمة',
),
),
if (fetchedModels.isNotEmpty) ...[
Expand Down Expand Up @@ -158,24 +158,24 @@ Future<void> configureAiProvider(
obscureText: true,
decoration: InputDecoration(
labelText: preset.authType == AiProviderAuthType.none
? 'API Key(本地服务通常不需要)'
? 'مفتاح واجهة برمجة التطبيقات (عادةً غير مطلوب للخدمات المحلية)'
: editing == null
? 'API Key'
: 'API Key(留空保持不变)',
: 'مفتاح واجهة برمجة التطبيقات (اتركه فارغًا لتركه دون تغيير)',
),
),
const SizedBox(height: 8),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text('Agent 工具'),
subtitle: const Text('允许模型读取目录、标注和书中原文'),
title: const Text('أدوات الوكيل'),
subtitle: const Text('يسمح للنموذج بقراءة الكتالوجات والتعليقات التوضيحية والنص الأصلي في الكتب'),
value: toolsEnabled,
onChanged: (value) => setState(() => toolsEnabled = value),
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text('思考摘要'),
subtitle: const Text('显示服务返回的可见 reasoning 内容'),
title: const Text('ملخص التفكير'),
subtitle: const Text('عرض محتوى الاستدلال المرئي الذي أرجعته الخدمة'),
value: reasoningEnabled,
onChanged: (value) =>
setState(() => reasoningEnabled = value),
Expand All @@ -196,7 +196,7 @@ Future<void> configureAiProvider(
if (editing != null)
TextButton.icon(
onPressed: () async {
setState(() => probeStatus = '正在测试连接…');
setState(() => probeStatus = 'جارٍ اختبار الاتصال...');
try {
final result = await ref
.read(chatControllerProvider.notifier)
Expand All @@ -205,17 +205,17 @@ Future<void> configureAiProvider(
setState(() {
fetchedModels = result.models;
probeStatus = result.succeeded
? '连接成功 · HTTP ${result.statusCode} · ${result.latencyMillis} ms${result.models.isEmpty ? '' : ' · ${result.models.length} 个模型'}'
: '连接失败:${result.errorCode} · HTTP ${result.statusCode ?? '-'}';
? 'تم الاتصال بنجاح · HTTP${result.statusCode} · ${result.latencyMillis} ms${result.models.isEmpty ? '' : ' · ${result.models.length} نماذج'}'
: 'فشل اتصال!${result.errorCode} · HTTP ${result.statusCode ?? '-'}';
});
} on Object catch (error) {
if (context.mounted) {
setState(() => probeStatus = '连接失败:$error');
setState(() => probeStatus = 'فشل الاتصال: $error');
}
}
},
icon: const Icon(Icons.network_check),
label: const Text('测试并拉取模型'),
label: const Text('اختبار النموذج وسحبه'),
),
if (editing != null && !editing!.isActive)
TextButton(
Expand All @@ -225,7 +225,7 @@ Future<void> configureAiProvider(
.activateProvider(editing!.id);
if (context.mounted) Navigator.pop(context, false);
},
child: const Text('设为当前'),
child: const Text('تعيين كتيار'),
),
if (editing != null)
TextButton(
Expand All @@ -235,15 +235,15 @@ Future<void> configureAiProvider(
.setProviderEnabled(editing!.id, !editing!.isEnabled);
if (context.mounted) Navigator.pop(context, false);
},
child: Text(editing!.isEnabled ? '停用' : '启用'),
child: Text(editing!.isEnabled ? 'تعطيل' : 'تمكين '),
),
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('取消'),
child: const Text('إلغاء'),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('保存'),
child: const Text('حفظ'),
),
],
),
Expand Down Expand Up @@ -279,7 +279,7 @@ Future<void> configureAiProvider(
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('保存模型配置失败:$error')));
).showSnackBar(SnackBar(content: Text('فشل حفظ تكوين النموذج: $error')));
}
} finally {
name.dispose();
Expand Down
Loading