feat: 完善芽说英语品牌Logo与图标配置,完成跨平台云端同步服务开发与自动化部署

This commit is contained in:
shen
2026-09-16 09:15:53 +08:00
parent 293b838341
commit eda5dfce62
66 changed files with 3564 additions and 80 deletions
@@ -123,6 +123,8 @@ class _DialoguePageState extends State<DialoguePage> {
bool waitingForReply = false;
String? validationError;
final Set<int> _shownTranslations = <int>{};
LessonDialogue get script => widget.isLessonDialogue
? dialogueBySegmentId(
lessonById(widget.state.activeLessonId)
@@ -136,8 +138,63 @@ class _DialoguePageState extends State<DialoguePage> {
goal: '姓名、地点、状态或喜好,并反问',
prompts: prompts,
hints: hints,
translations: translations,
);
String? _resolveTranslationFor(String text, int currentStage) {
final cleanText = text.trim();
// 1. Check current script prompts
for (var i = 0; i < script.prompts.length; i++) {
if (script.prompts[i].trim().toLowerCase() == cleanText.toLowerCase()) {
if (i < script.translations.length) {
return script.translations[i];
}
}
}
// 2. Check standalone prompts
for (var i = 0; i < prompts.length; i++) {
if (prompts[i].trim().toLowerCase() == cleanText.toLowerCase()) {
if (i < translations.length) {
return translations[i];
}
}
}
// 3. Check all lesson dialogues
for (final dialogue in a0Dialogues.values) {
for (var i = 0; i < dialogue.prompts.length; i++) {
if (dialogue.prompts[i].trim().toLowerCase() == cleanText.toLowerCase()) {
if (i < dialogue.translations.length) {
return dialogue.translations[i];
}
}
}
}
// 4. Check all segment dialogues
for (final dialogue in a0SegmentDialogues.values) {
for (var i = 0; i < dialogue.prompts.length; i++) {
if (dialogue.prompts[i].trim().toLowerCase() == cleanText.toLowerCase()) {
if (i < dialogue.translations.length) {
return dialogue.translations[i];
}
}
}
}
// 5. Common fallback phrases
if (cleanText.toLowerCase().contains("wonderful") &&
cleanText.toLowerCase().contains("nice meeting you")) {
return "太棒了 — 很高兴认识你!";
}
if (cleanText.toLowerCase().contains("goodbye") ||
cleanText.toLowerCase().contains("bye")) {
return "再见!";
}
// 6. If stage index is within script.translations
if (currentStage >= 0 && currentStage < script.translations.length) {
return script.translations[currentStage];
}
return null;
}
static const prompts = [
'Hi! My name is Mia. Whats your name?',
'Nice to meet you. Where are you from?',
@@ -170,9 +227,26 @@ class _DialoguePageState extends State<DialoguePage> {
if (canRestore) {
stage = draft.stage;
usedHelp = draft.usedHelp;
turns.addAll(draft.turns);
for (var i = 0; i < draft.turns.length; i++) {
final t = draft.turns[i];
if (!t.isLearner && (t.translation == null || t.translation!.isEmpty)) {
final trans = _resolveTranslationFor(t.text, i ~/ 2);
turns.add(t.copyWith(translation: trans));
} else {
turns.add(t);
}
}
} else {
turns.add(DialogueTurn(text: script.prompts.first, isLearner: false));
final initialPrompt = script.prompts.first;
final initialTranslation = script.translations.firstOrNull ??
_resolveTranslationFor(initialPrompt, 0);
turns.add(
DialogueTurn(
text: initialPrompt,
isLearner: false,
translation: initialTranslation,
),
);
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
@@ -245,11 +319,16 @@ class _DialoguePageState extends State<DialoguePage> {
(nextStage < script.prompts.length
? script.prompts[nextStage]
: 'Wonderful — nice meeting you!');
var replyTranslation = aiResponse?.translation;
if (replyTranslation == null || replyTranslation.isEmpty) {
replyTranslation = _resolveTranslationFor(replyText, nextStage);
}
setState(() {
turns.add(
DialogueTurn(
text: replyText,
isLearner: false,
translation: replyTranslation,
),
);
waitingForReply = false;
@@ -360,6 +439,94 @@ class _DialoguePageState extends State<DialoguePage> {
);
}
Future<void> _toggleTurnTranslation(int index) async {
if (index < 0 || index >= turns.length) return;
final turn = turns[index];
if (turn.isLearner) return;
if (_shownTranslations.contains(index)) {
setState(() {
_shownTranslations.remove(index);
});
return;
}
String? trans = turn.translation;
if (trans == null || trans.isEmpty) {
trans = _resolveTranslationFor(turn.text, index ~/ 2);
}
if (trans != null && trans.isNotEmpty) {
setState(() {
turns[index] = turn.copyWith(translation: trans);
_shownTranslations.add(index);
});
_saveDraft();
return;
}
setState(() {
_shownTranslations.add(index);
turns[index] = turn.copyWith(translation: "正在翻译…");
});
final fetched = await AiService.instance.temporaryDefinition(
provider: widget.state.aiProvider,
endpoint: widget.state.aiEndpoint,
model: widget.state.aiModel,
text: turn.text,
);
if (!mounted) return;
final finalTrans =
(fetched != null && fetched.isNotEmpty) ? fetched : "暂无该句中文翻译";
setState(() {
turns[index] = turn.copyWith(translation: finalTrans);
});
_saveDraft();
}
Future<void> _showLatestAiTranslation() async {
final latestAiIndex = turns.lastIndexWhere((turn) => !turn.isLearner);
if (latestAiIndex == -1) return;
final latestAi = turns[latestAiIndex];
String? trans = latestAi.translation;
if (trans == null || trans.isEmpty) {
trans = _resolveTranslationFor(latestAi.text, stage);
}
if (trans != null && trans.isNotEmpty) {
setState(() {
usedHelp = true;
hint = "对方说:$trans";
_shownTranslations.add(latestAiIndex);
turns[latestAiIndex] = latestAi.copyWith(translation: trans);
});
_saveDraft();
return;
}
setState(() {
usedHelp = true;
hint = "正在获取对方英文翻译…";
_shownTranslations.add(latestAiIndex);
});
final fetched = await AiService.instance.temporaryDefinition(
provider: widget.state.aiProvider,
endpoint: widget.state.aiEndpoint,
model: widget.state.aiModel,
text: latestAi.text,
);
if (!mounted) return;
final finalTrans =
(fetched != null && fetched.isNotEmpty) ? fetched : "暂无该句中文翻译";
setState(() {
hint = "对方说:$finalTrans";
turns[latestAiIndex] = latestAi.copyWith(translation: finalTrans);
});
_saveDraft();
}
Future<void> _playLatestAi({required bool slow}) async {
final latest = turns.where((turn) => !turn.isLearner).lastOrNull;
if (latest == null) return;
@@ -553,28 +720,83 @@ class _DialoguePageState extends State<DialoguePage> {
children: [
LexiconText(turn.text, state: widget.state),
if (!turn.isLearner) ...[
const SizedBox(height: 6),
GestureDetector(
onTap: () => VoiceService.instance.speak(turn.text),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.volume_up_outlined,
size: 16,
color: AppColors.green,
if (_shownTranslations.contains(index) &&
turn.translation != null &&
turn.translation!.isNotEmpty) ...[
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(6),
),
child: Text(
turn.translation!,
style: const TextStyle(
fontSize: 13,
color: Color(0xFF2D3748),
),
SizedBox(width: 4),
Text(
"播放",
style: TextStyle(
fontSize: 12,
color: AppColors.green,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
const SizedBox(height: 6),
Row(
mainAxisSize: MainAxisSize.min,
children: [
GestureDetector(
onTap: () =>
VoiceService.instance.speak(turn.text),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.volume_up_outlined,
size: 16,
color: AppColors.green,
),
SizedBox(width: 4),
Text(
"播放",
style: TextStyle(
fontSize: 12,
color: AppColors.green,
fontWeight: FontWeight.w500,
),
),
],
),
),
const SizedBox(width: 14),
GestureDetector(
onTap: () => _toggleTurnTranslation(index),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_shownTranslations.contains(index)
? Icons.translate
: Icons.translate_outlined,
size: 16,
color: AppColors.green,
),
const SizedBox(width: 4),
Text(
_shownTranslations.contains(index)
? "隐藏翻译"
: "翻译",
style: const TextStyle(
fontSize: 12,
color: AppColors.green,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
],
],
@@ -594,20 +816,17 @@ class _DialoguePageState extends State<DialoguePage> {
onTap: () {
setState(() {
usedHelp = true;
hint = script.hints[stage];
final hintIdx = stage < script.hints.length
? stage
: (script.hints.isNotEmpty ? script.hints.length - 1 : 0);
hint = script.hints.isNotEmpty ? script.hints[hintIdx] : null;
});
_saveDraft();
},
),
_AssistChip(
label: '翻译',
onTap: () {
setState(() {
usedHelp = true;
hint = translations[stage];
});
_saveDraft();
},
onTap: _showLatestAiTranslation,
),
_AssistChip(
label: '慢一点',