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
4 changes: 4 additions & 0 deletions lib/desktop/desktop_settings_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:re_editor/re_editor.dart';
import 'dart:convert';
import 'dart:ui' as ui;

Expand All @@ -16,6 +17,8 @@ import 'model_fetch_dialog.dart' show showModelFetchDialog;
import 'widgets/desktop_select_dropdown.dart';
import '../shared/widgets/ios_switch.dart';
import '../shared/widgets/ios_checkbox.dart';
import '../shared/widgets/input_height_constraints.dart';
import '../shared/widgets/plain_text_code_editor.dart';
// Desktop assistants panel dependencies
import '../features/assistant/pages/assistant_settings_edit_page.dart'
show showAssistantDesktopDialog; // dialog opener only
Expand All @@ -25,6 +28,7 @@ import '../utils/avatar_cache.dart';
import '../utils/sandbox_path_resolver.dart';
import 'dart:io' show Directory, File, Platform;
import '../utils/app_directories.dart';
import '../utils/re_editor_utils.dart';
import 'add_provider_dialog.dart' show showDesktopAddProviderDialog;
import 'model_edit_dialog.dart'
show showDesktopCreateModelDialog, showDesktopModelEditDialog;
Expand Down
120 changes: 79 additions & 41 deletions lib/desktop/desktop_translate_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:re_editor/re_editor.dart';

import '../icons/lucide_adapter.dart' as lucide;
import '../l10n/app_localizations.dart';
Expand All @@ -11,10 +12,12 @@ import '../core/providers/settings_provider.dart';
import '../core/providers/assistant_provider.dart';
import '../core/services/api/chat_api_service.dart';
import '../shared/widgets/snackbar.dart';
import '../shared/widgets/plain_text_code_editor.dart';
import '../features/model/widgets/model_select_sheet.dart'
show showModelSelector;
import '../features/settings/widgets/language_select_sheet.dart'
show LanguageOption, supportedLanguages;
import '../utils/re_editor_utils.dart';

class DesktopTranslatePage extends StatefulWidget {
const DesktopTranslatePage({super.key});
Expand All @@ -24,15 +27,16 @@ class DesktopTranslatePage extends StatefulWidget {
}

class _DesktopTranslatePageState extends State<DesktopTranslatePage> {
final TextEditingController _source = TextEditingController();
final TextEditingController _output = TextEditingController();
final CodeLineEditingController _source = CodeLineEditingController();
final CodeLineEditingController _output = CodeLineEditingController();

LanguageOption? _targetLang;
String? _modelProviderKey;
String? _modelId;

StreamSubscription? _subscription;
bool _translating = false;
int _translateRunId = 0;

@override
void initState() {
Expand Down Expand Up @@ -133,11 +137,15 @@ class _DesktopTranslatePageState extends State<DesktopTranslatePage> {
}

Future<void> _startTranslate() async {
if (_translating) return;
final l10n = AppLocalizations.of(context)!;
final settings = context.read<SettingsProvider>();

final text = _source.text.trim();
if (text.isEmpty) return;
await _subscription?.cancel();
_subscription = null;
if (!mounted) return;

final providerKey = _modelProviderKey;
final modelId = _modelId;
Expand All @@ -159,9 +167,25 @@ class _DesktopTranslatePageState extends State<DesktopTranslatePage> {

setState(() {
_translating = true;
_output.text = '';
_output.value = const CodeLineEditingValue.empty();
});

final runId = ++_translateRunId;
final buffer = StringBuffer();
Timer? flushTimer;
void flushNow() {
if (!mounted || runId != _translateRunId) return;
_setOutputText(buffer.toString());
}

void scheduleFlush() {
if (flushTimer?.isActive ?? false) return;
flushTimer = Timer(const Duration(milliseconds: 80), () {
flushTimer = null;
flushNow();
});
}

try {
final stream = ChatApiService.sendMessageStream(
config: cfg,
Expand All @@ -174,19 +198,27 @@ class _DesktopTranslatePageState extends State<DesktopTranslatePage> {
_subscription = stream.listen(
(chunk) {
// live update; remove leading whitespace on first chunk to avoid top gap
if (runId != _translateRunId) return;
final s = chunk.content;
if (_output.text.isEmpty) {
_output.text = s.replaceFirst(RegExp(r'^\s+'), '');
if (buffer.isEmpty) {
buffer.write(s.replaceFirst(RegExp(r'^\s+'), ''));
} else {
_output.text += s;
buffer.write(s);
}
scheduleFlush();
},
onDone: () {
if (!mounted) return;
if (!mounted || runId != _translateRunId) return;
flushTimer?.cancel();
flushNow();
_subscription = null;
setState(() => _translating = false);
},
onError: (e) {
if (!mounted) return;
if (!mounted || runId != _translateRunId) return;
flushTimer?.cancel();
flushNow();
_subscription = null;
setState(() => _translating = false);
showAppSnackBar(
context,
Expand All @@ -197,6 +229,8 @@ class _DesktopTranslatePageState extends State<DesktopTranslatePage> {
cancelOnError: true,
);
} catch (e) {
_subscription = null;
if (!mounted) return;
setState(() => _translating = false);
showAppSnackBar(
context,
Expand All @@ -210,9 +244,15 @@ class _DesktopTranslatePageState extends State<DesktopTranslatePage> {
try {
await _subscription?.cancel();
} catch (_) {}
_subscription = null;
_translateRunId++;
if (mounted) setState(() => _translating = false);
}

void _setOutputText(String text) {
_output.setTextSafely(text);
}

@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
Expand Down Expand Up @@ -295,26 +335,23 @@ class _DesktopTranslatePageState extends State<DesktopTranslatePage> {
overlay: _PaneActionButton(
icon: lucide.Lucide.Eraser,
label: l10n.translatePageClearAll,
onTap: () {
_source.clear();
_output.clear();
onTap: () async {
if (_translating || _subscription != null) {
await _stopTranslate();
}
_source.value =
const CodeLineEditingValue.empty();
_output.value =
const CodeLineEditingValue.empty();
},
),
child: TextField(
child: PlainTextCodeEditor(
controller: _source,
keyboardType: TextInputType.multiline,
maxLines: null,
expands: true,
decoration: InputDecoration(
hintText: l10n.translatePageInputHint,
border: InputBorder.none,
isCollapsed: true,
contentPadding: const EdgeInsets.all(14),
),
style: const TextStyle(
fontSize: 14.5,
height: 1.4,
),
autofocus: false,
hint: l10n.translatePageInputHint,
padding: const EdgeInsets.all(14),
fontSize: 14.5,
fontHeight: 1.4,
),
),
),
Expand All @@ -337,22 +374,14 @@ class _DesktopTranslatePageState extends State<DesktopTranslatePage> {
);
},
),
child: TextField(
child: PlainTextCodeEditor(
controller: _output,
readOnly: true,
keyboardType: TextInputType.multiline,
maxLines: null,
expands: true,
decoration: InputDecoration(
hintText: l10n.translatePageOutputHint,
border: InputBorder.none,
isCollapsed: true,
contentPadding: const EdgeInsets.all(14),
),
style: const TextStyle(
fontSize: 14.5,
height: 1.4,
),
autofocus: false,
hint: l10n.translatePageOutputHint,
padding: const EdgeInsets.all(14),
fontSize: 14.5,
fontHeight: 1.4,
),
),
),
Expand Down Expand Up @@ -473,8 +502,15 @@ class _LanguageDropdownState extends State<_LanguageDropdown> {
);
},
);
Overlay.of(context).insert(_entry!);
setState(() => _open = true);
final overlay = Overlay.maybeOf(context);
if (overlay == null) {
_entry = null;
return;
}
overlay.insert(_entry!);
if (mounted) {
setState(() => _open = true);
}
}

@override
Expand Down Expand Up @@ -882,6 +918,7 @@ class _TranslateButtonState extends State<_TranslateButton> {
colorFilter: ColorFilter.mode(fg, BlendMode.srcIn),
),
const SizedBox(width: 6),
// TODO: Replace hard-coded label with AppLocalizations (i18n).
Text(
l10n.chatMessageWidgetStopTooltip,
style: TextStyle(
Expand All @@ -898,6 +935,7 @@ class _TranslateButtonState extends State<_TranslateButton> {
children: [
Icon(lucide.Lucide.Languages, size: 16, color: fg),
const SizedBox(width: 6),
// TODO: Replace hard-coded label with AppLocalizations (i18n).
Text(
l10n.chatMessageWidgetTranslateTooltip,
style: TextStyle(
Expand Down
Loading
Loading