整个课程和数据UI重新设计版本
This commit is contained in:
@@ -5,14 +5,15 @@ import '../core/app_state.dart';
|
||||
import '../core/app_theme.dart';
|
||||
import '../core/ai_service.dart';
|
||||
import '../core/models.dart';
|
||||
import '../core/seed_courses.dart';
|
||||
import '../core/courses/courses.dart';
|
||||
import '../core/voice_service.dart';
|
||||
import 'app_widgets.dart';
|
||||
|
||||
List<VocabularyItem> get courseLexiconEntries {
|
||||
// Segment words first: they carry the example and IPA a lesson shows.
|
||||
final entries = <VocabularyItem>[
|
||||
...a0SeedLessons.expand((lesson) => lesson.vocabulary),
|
||||
...a0SegmentVocabulary.values.expand((items) => items),
|
||||
...segmentVocabulary.values.expand((items) => items),
|
||||
...allLessons.expand((lesson) => lesson.vocabulary),
|
||||
];
|
||||
final seen = <String>{};
|
||||
return entries.where((item) => seen.add(item.word.toLowerCase())).toList()
|
||||
@@ -136,9 +137,7 @@ class _LexiconTextState extends State<LexiconText> {
|
||||
final expression = RegExp(
|
||||
r'(?<![a-zA-Z0-9])(?:' +
|
||||
entries
|
||||
.map(
|
||||
(item) => RegExp.escape(item.word).replaceAll('’', "['’]"),
|
||||
)
|
||||
.map((item) => RegExp.escape(item.word).replaceAll('’', "['’]"))
|
||||
.join('|') +
|
||||
r')(?![a-zA-Z0-9])',
|
||||
caseSensitive: false,
|
||||
@@ -315,8 +314,8 @@ class _LexiconLookupSheetState extends State<_LexiconLookupSheet> {
|
||||
sentenceAnalysis = result;
|
||||
sentenceAnalysisError = result == null
|
||||
? (widget.state.aiProvider.name == 'mock'
|
||||
? '未能完成解析,请稍后重试。'
|
||||
: 'AI 解析暂不可用,请检查网络或 AI 服务配置。')
|
||||
? '未能完成解析,请稍后重试。'
|
||||
: 'AI 解析暂不可用,请检查网络或 AI 服务配置。')
|
||||
: null;
|
||||
});
|
||||
|
||||
@@ -571,24 +570,41 @@ class _LexiconLookupSheetState extends State<_LexiconLookupSheet> {
|
||||
IconButton(
|
||||
icon: const Icon(Icons.volume_up_outlined, size: 20),
|
||||
tooltip: '播放读音',
|
||||
onPressed: () => VoiceService.instance.speak(phrase.phrase),
|
||||
onPressed: () =>
|
||||
VoiceService.instance.speak(phrase.phrase),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
_addedToReview.contains(phrase.phrase.toLowerCase())
|
||||
? Chip(
|
||||
label: Text('已在复习', style: TextStyle(fontSize: 12)),
|
||||
avatar: Icon(Icons.check, size: 14, color: AppColors.green),
|
||||
label: Text(
|
||||
'已在复习',
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
avatar: Icon(
|
||||
Icons.check,
|
||||
size: 14,
|
||||
color: AppColors.green,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
)
|
||||
: OutlinedButton.icon(
|
||||
style: OutlinedButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
),
|
||||
onPressed: () => _addPhraseToReview(phrase),
|
||||
icon: const Icon(Icons.bookmark_add_outlined, size: 14),
|
||||
label: const Text('加复习', style: TextStyle(fontSize: 12)),
|
||||
icon: const Icon(
|
||||
Icons.bookmark_add_outlined,
|
||||
size: 14,
|
||||
),
|
||||
label: const Text(
|
||||
'加复习',
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -602,10 +618,7 @@ class _LexiconLookupSheetState extends State<_LexiconLookupSheet> {
|
||||
if (phrase.usageNote != null && phrase.usageNote!.isNotEmpty)
|
||||
Text(
|
||||
'用法:${phrase.usageNote}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.muted,
|
||||
),
|
||||
style: TextStyle(fontSize: 12, color: AppColors.muted),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -621,7 +634,9 @@ class _LexiconLookupSheetState extends State<_LexiconLookupSheet> {
|
||||
style: TextStyle(fontSize: 12, color: AppColors.muted),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: requestingSentenceAnalysis ? null : _requestSentenceAnalysis,
|
||||
onPressed: requestingSentenceAnalysis
|
||||
? null
|
||||
: _requestSentenceAnalysis,
|
||||
icon: const Icon(Icons.refresh, size: 14),
|
||||
label: const Text('重新解析', style: TextStyle(fontSize: 12)),
|
||||
),
|
||||
@@ -716,7 +731,10 @@ class _LexiconLookupSheetState extends State<_LexiconLookupSheet> {
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () =>
|
||||
VoiceService.instance.speak(queryText, slow: true),
|
||||
icon: const Icon(Icons.slow_motion_video_outlined, size: 18),
|
||||
icon: const Icon(
|
||||
Icons.slow_motion_video_outlined,
|
||||
size: 18,
|
||||
),
|
||||
label: const Text('慢速朗读'),
|
||||
),
|
||||
),
|
||||
@@ -730,7 +748,10 @@ class _LexiconLookupSheetState extends State<_LexiconLookupSheet> {
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
if (entry!.ipa != null)
|
||||
Text(entry!.ipa!, style: Theme.of(context).textTheme.bodyMedium),
|
||||
Text(
|
||||
entry!.ipa!,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
Text(entry!.meaning, style: const TextStyle(fontSize: 18)),
|
||||
SectionCard(
|
||||
tint: AppColors.softGreen,
|
||||
@@ -740,7 +761,8 @@ class _LexiconLookupSheetState extends State<_LexiconLookupSheet> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
label: _addedToReview.contains(entry!.word.toLowerCase())
|
||||
label:
|
||||
_addedToReview.contains(entry!.word.toLowerCase())
|
||||
? '已在复习中'
|
||||
: '加入复习',
|
||||
onPressed: () {
|
||||
@@ -753,9 +775,13 @@ class _LexiconLookupSheetState extends State<_LexiconLookupSheet> {
|
||||
// Provide option to analyze further with AI if user wants deeper context
|
||||
if (sentenceAnalysis == null)
|
||||
OutlinedButton.icon(
|
||||
onPressed: requestingSentenceAnalysis ? null : _requestSentenceAnalysis,
|
||||
onPressed: requestingSentenceAnalysis
|
||||
? null
|
||||
: _requestSentenceAnalysis,
|
||||
icon: const Icon(Icons.auto_awesome_outlined, size: 16),
|
||||
label: Text(requestingSentenceAnalysis ? 'AI 解析中…' : '请求 AI 句型与深度解析'),
|
||||
label: Text(
|
||||
requestingSentenceAnalysis ? 'AI 解析中…' : '请求 AI 句型与深度解析',
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -769,7 +795,11 @@ class _LexiconLookupSheetState extends State<_LexiconLookupSheet> {
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.layers_outlined, size: 18, color: AppColors.green),
|
||||
Icon(
|
||||
Icons.layers_outlined,
|
||||
size: 18,
|
||||
color: AppColors.green,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'本地词典匹配到的短语 (${localPhrases.length})',
|
||||
@@ -798,13 +828,22 @@ class _LexiconLookupSheetState extends State<_LexiconLookupSheet> {
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.volume_up_outlined, size: 18),
|
||||
icon: const Icon(
|
||||
Icons.volume_up_outlined,
|
||||
size: 18,
|
||||
),
|
||||
onPressed: () =>
|
||||
VoiceService.instance.speak(phraseItem.word),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
_addedToReview.contains(phraseItem.word.toLowerCase())
|
||||
? Icon(Icons.check, size: 18, color: AppColors.green)
|
||||
_addedToReview.contains(
|
||||
phraseItem.word.toLowerCase(),
|
||||
)
|
||||
? Icon(
|
||||
Icons.check,
|
||||
size: 18,
|
||||
color: AppColors.green,
|
||||
)
|
||||
: OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
@@ -815,11 +854,17 @@ class _LexiconLookupSheetState extends State<_LexiconLookupSheet> {
|
||||
),
|
||||
onPressed: () =>
|
||||
_addVocabItemToReview(phraseItem),
|
||||
child: const Text('+ 复习', style: TextStyle(fontSize: 12)),
|
||||
child: const Text(
|
||||
'+ 复习',
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(phraseItem.meaning, style: const TextStyle(fontSize: 14)),
|
||||
Text(
|
||||
phraseItem.meaning,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
if (phraseItem.example.isNotEmpty)
|
||||
Text(
|
||||
'例:${phraseItem.example} (${phraseItem.exampleMeaning})',
|
||||
@@ -903,7 +948,9 @@ class _LexiconLookupSheetState extends State<_LexiconLookupSheet> {
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
widget.state.removeTemporaryDefinition(controller.text);
|
||||
widget.state.removeTemporaryDefinition(
|
||||
controller.text,
|
||||
);
|
||||
setState(() => temporaryDefinition = null);
|
||||
},
|
||||
icon: const Icon(Icons.delete_outline, size: 18),
|
||||
|
||||
Reference in New Issue
Block a user