feat: localisation with en, de, fr and it
This commit is contained in:
@@ -15,6 +15,7 @@ import '../../../shared/theme/theme_extensions.dart';
|
||||
import '../../../shared/widgets/conduit_components.dart';
|
||||
import '../../../core/auth/auth_state_manager.dart';
|
||||
import '../../../core/utils/debug_logger.dart';
|
||||
import 'package:conduit/l10n/app_localizations.dart';
|
||||
|
||||
class AuthenticationPage extends ConsumerStatefulWidget {
|
||||
final ServerConfig serverConfig;
|
||||
@@ -61,6 +62,7 @@ class _AuthenticationPageState extends ConsumerState<AuthenticationPage> {
|
||||
}
|
||||
|
||||
Future<void> _signIn() async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
setState(() {
|
||||
@@ -87,7 +89,7 @@ class _AuthenticationPageState extends ConsumerState<AuthenticationPage> {
|
||||
|
||||
if (!success) {
|
||||
final authState = ref.read(authStateManagerProvider);
|
||||
throw Exception(authState.error ?? 'Login failed');
|
||||
throw Exception(authState.error ?? l10n.loginFailed);
|
||||
}
|
||||
|
||||
// Success - navigation will be handled by auth state change
|
||||
@@ -106,15 +108,15 @@ class _AuthenticationPageState extends ConsumerState<AuthenticationPage> {
|
||||
|
||||
String _formatLoginError(String error) {
|
||||
if (error.contains('401') || error.contains('Unauthorized')) {
|
||||
return 'Invalid username or password. Please try again.';
|
||||
return AppLocalizations.of(context)!.invalidCredentials;
|
||||
} else if (error.contains('redirect')) {
|
||||
return 'The server is redirecting requests. Check your server\'s HTTPS configuration.';
|
||||
return AppLocalizations.of(context)!.serverRedirectingHttps;
|
||||
} else if (error.contains('SocketException')) {
|
||||
return 'Unable to connect to server. Please check your connection.';
|
||||
return AppLocalizations.of(context)!.unableToConnectServer;
|
||||
} else if (error.contains('timeout')) {
|
||||
return 'The request timed out. Please try again.';
|
||||
return AppLocalizations.of(context)!.requestTimedOut;
|
||||
}
|
||||
return 'We couldn\'t sign you in. Check your credentials and server settings.';
|
||||
return AppLocalizations.of(context)!.genericSignInFailed;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -201,7 +203,7 @@ class _AuthenticationPageState extends ConsumerState<AuthenticationPage> {
|
||||
ConduitIconButton(
|
||||
icon: Platform.isIOS ? CupertinoIcons.back : Icons.arrow_back,
|
||||
onPressed: () => Navigator.pop(context),
|
||||
tooltip: 'Back to server setup',
|
||||
tooltip: AppLocalizations.of(context)!.backToServerSetup,
|
||||
),
|
||||
const Spacer(),
|
||||
// Progress indicator (step 2 of 2)
|
||||
@@ -263,7 +265,7 @@ class _AuthenticationPageState extends ConsumerState<AuthenticationPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Connected to Server',
|
||||
AppLocalizations.of(context)!.connectedToServer,
|
||||
style: context.conduitTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.conduitTheme.success,
|
||||
@@ -302,7 +304,7 @@ class _AuthenticationPageState extends ConsumerState<AuthenticationPage> {
|
||||
),
|
||||
const SizedBox(height: Spacing.lg),
|
||||
Text(
|
||||
'Sign In',
|
||||
AppLocalizations.of(context)!.signIn,
|
||||
textAlign: TextAlign.center,
|
||||
style: context.conduitTheme.headingLarge?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
@@ -314,7 +316,7 @@ class _AuthenticationPageState extends ConsumerState<AuthenticationPage> {
|
||||
),
|
||||
const SizedBox(height: Spacing.sm),
|
||||
Text(
|
||||
'Enter your credentials to access your AI conversations',
|
||||
AppLocalizations.of(context)!.enterCredentials,
|
||||
textAlign: TextAlign.center,
|
||||
style: context.conduitTheme.bodyLarge?.copyWith(
|
||||
color: context.conduitTheme.textSecondary,
|
||||
@@ -370,7 +372,7 @@ class _AuthenticationPageState extends ConsumerState<AuthenticationPage> {
|
||||
icon: Platform.isIOS
|
||||
? CupertinoIcons.person_circle
|
||||
: Icons.account_circle_outlined,
|
||||
label: 'Credentials',
|
||||
label: AppLocalizations.of(context)!.credentials,
|
||||
isSelected: !_useApiKey,
|
||||
onTap: () => setState(() => _useApiKey = false),
|
||||
),
|
||||
@@ -380,7 +382,7 @@ class _AuthenticationPageState extends ConsumerState<AuthenticationPage> {
|
||||
icon: Platform.isIOS
|
||||
? CupertinoIcons.lock_shield
|
||||
: Icons.vpn_key_outlined,
|
||||
label: 'API Key',
|
||||
label: AppLocalizations.of(context)!.apiKey,
|
||||
isSelected: _useApiKey,
|
||||
onTap: () => setState(() => _useApiKey = true),
|
||||
),
|
||||
@@ -469,7 +471,7 @@ class _AuthenticationPageState extends ConsumerState<AuthenticationPage> {
|
||||
key: const ValueKey('api_key_form'),
|
||||
children: [
|
||||
AccessibleFormField(
|
||||
label: 'API Key',
|
||||
label: AppLocalizations.of(context)!.apiKey,
|
||||
hint: 'sk-...',
|
||||
controller: _apiKeyController,
|
||||
validator: InputValidationService.combine([
|
||||
@@ -477,7 +479,7 @@ class _AuthenticationPageState extends ConsumerState<AuthenticationPage> {
|
||||
(value) => InputValidationService.validateMinLength(
|
||||
value,
|
||||
10,
|
||||
fieldName: 'API Key',
|
||||
fieldName: AppLocalizations.of(context)!.apiKey,
|
||||
),
|
||||
]),
|
||||
obscureText: _obscurePassword,
|
||||
@@ -513,7 +515,7 @@ class _AuthenticationPageState extends ConsumerState<AuthenticationPage> {
|
||||
key: const ValueKey('credentials_form'),
|
||||
children: [
|
||||
AccessibleFormField(
|
||||
label: 'Username or Email',
|
||||
label: AppLocalizations.of(context)!.usernameOrEmail,
|
||||
hint: 'Enter your username or email',
|
||||
controller: _usernameController,
|
||||
validator: InputValidationService.combine([
|
||||
@@ -531,7 +533,7 @@ class _AuthenticationPageState extends ConsumerState<AuthenticationPage> {
|
||||
),
|
||||
const SizedBox(height: Spacing.lg),
|
||||
AccessibleFormField(
|
||||
label: 'Password',
|
||||
label: AppLocalizations.of(context)!.password,
|
||||
hint: 'Enter your password',
|
||||
controller: _passwordController,
|
||||
validator: InputValidationService.combine([
|
||||
@@ -576,8 +578,8 @@ class _AuthenticationPageState extends ConsumerState<AuthenticationPage> {
|
||||
text: _isSigningIn
|
||||
? 'Signing in...'
|
||||
: _useApiKey
|
||||
? 'Sign in with API Key'
|
||||
: 'Sign In',
|
||||
? AppLocalizations.of(context)!.signInWithApiKey
|
||||
: AppLocalizations.of(context)!.signIn,
|
||||
icon: _isSigningIn
|
||||
? null
|
||||
: (Platform.isIOS
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../core/widgets/error_boundary.dart';
|
||||
import '../../../shared/theme/theme_extensions.dart';
|
||||
import '../../../shared/widgets/conduit_components.dart';
|
||||
import 'package:conduit/l10n/app_localizations.dart';
|
||||
import 'server_connection_page.dart';
|
||||
|
||||
/// Entry point for the connection and sign-in flow
|
||||
@@ -26,12 +27,12 @@ class ConnectAndSignInPage extends ConsumerWidget {
|
||||
return ErrorBoundary(
|
||||
child: Scaffold(
|
||||
backgroundColor: context.conduitTheme.surfaceBackground,
|
||||
body: const Center(
|
||||
body: Center(
|
||||
child: ConduitLoadingIndicator(
|
||||
message: 'Loading...',
|
||||
message: AppLocalizations.of(context)!.loadingContent,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:conduit/l10n/app_localizations.dart';
|
||||
|
||||
import '../../../core/models/server_config.dart';
|
||||
import '../../../core/providers/app_providers.dart';
|
||||
@@ -116,7 +117,7 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
|
||||
|
||||
String _validateAndFormatUrl(String input) {
|
||||
if (input.isEmpty) {
|
||||
throw Exception('Server URL cannot be empty');
|
||||
throw Exception(AppLocalizations.of(context)!.serverUrlEmpty);
|
||||
}
|
||||
|
||||
// Clean up the input
|
||||
@@ -135,29 +136,29 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
|
||||
// Parse and validate the URI
|
||||
final uri = Uri.tryParse(url);
|
||||
if (uri == null) {
|
||||
throw Exception('Invalid URL format. Please check your input.');
|
||||
throw Exception(AppLocalizations.of(context)!.invalidUrlFormat);
|
||||
}
|
||||
|
||||
// Validate scheme
|
||||
if (uri.scheme != 'http' && uri.scheme != 'https') {
|
||||
throw Exception('Only HTTP and HTTPS protocols are supported.');
|
||||
throw Exception(AppLocalizations.of(context)!.onlyHttpHttps);
|
||||
}
|
||||
|
||||
// Validate host
|
||||
if (uri.host.isEmpty) {
|
||||
throw Exception('Server address is required (e.g., 192.168.1.10 or example.com).');
|
||||
throw Exception(AppLocalizations.of(context)!.serverAddressRequired);
|
||||
}
|
||||
|
||||
// Validate port if specified
|
||||
if (uri.hasPort) {
|
||||
if (uri.port < 1 || uri.port > 65535) {
|
||||
throw Exception('Port must be between 1 and 65535.');
|
||||
throw Exception(AppLocalizations.of(context)!.portRange);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate IP address format if it looks like an IP
|
||||
if (_isIPAddress(uri.host) && !_isValidIPAddress(uri.host)) {
|
||||
throw Exception('Invalid IP address format. Use format like 192.168.1.10.');
|
||||
throw Exception(AppLocalizations.of(context)!.invalidIpFormat);
|
||||
}
|
||||
|
||||
return url;
|
||||
@@ -192,15 +193,15 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
|
||||
|
||||
// Handle specific error types
|
||||
if (error.contains('SocketException')) {
|
||||
return 'We couldn\'t reach the server. Check your connection and that the server is running.';
|
||||
return AppLocalizations.of(context)!.weCouldntReachServer;
|
||||
} else if (error.contains('timeout')) {
|
||||
return 'Connection timed out. The server might be busy or blocked by a firewall.';
|
||||
return AppLocalizations.of(context)!.connectionTimedOut;
|
||||
} else if (error.contains('Server URL cannot be empty')) {
|
||||
return 'Please enter a server address.';
|
||||
return AppLocalizations.of(context)!.serverUrlEmpty;
|
||||
} else if (error.contains('Invalid URL format')) {
|
||||
return 'Invalid server address format. Examples:\n• 192.168.1.10:3000\n• example.com\n• https://myserver.com';
|
||||
return AppLocalizations.of(context)!.invalidUrlFormat;
|
||||
} else if (error.contains('Only HTTP and HTTPS')) {
|
||||
return 'Use http:// or https:// only.';
|
||||
return AppLocalizations.of(context)!.useHttpOrHttpsOnly;
|
||||
} else if (error.contains('Server address is required')) {
|
||||
return cleanError;
|
||||
} else if (error.contains('Port must be between')) {
|
||||
@@ -208,10 +209,10 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
|
||||
} else if (error.contains('Invalid IP address format')) {
|
||||
return cleanError;
|
||||
} else if (error.contains('This does not appear to be an Open-WebUI server')) {
|
||||
return 'This server doesn\'t appear to be running Open-WebUI. Please check the address.';
|
||||
return AppLocalizations.of(context)!.serverNotOpenWebUI;
|
||||
}
|
||||
|
||||
return 'Couldn\'t connect. Double-check the address and try again.';
|
||||
return AppLocalizations.of(context)!.couldNotConnectGeneric;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -379,7 +380,7 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
'Connect to Server',
|
||||
AppLocalizations.of(context)!.connectToServer,
|
||||
textAlign: TextAlign.center,
|
||||
style: context.conduitTheme.headingLarge?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
@@ -391,7 +392,7 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
|
||||
),
|
||||
const SizedBox(height: Spacing.sm),
|
||||
Text(
|
||||
'Enter your Open-WebUI server address to get started',
|
||||
AppLocalizations.of(context)!.enterServerAddress,
|
||||
textAlign: TextAlign.center,
|
||||
style: context.conduitTheme.bodyLarge?.copyWith(
|
||||
color: context.conduitTheme.textSecondary,
|
||||
@@ -611,7 +612,7 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: AccessibleFormField(
|
||||
label: 'Header Name',
|
||||
label: AppLocalizations.of(context)!.headerName,
|
||||
hint: 'X-Custom-Header',
|
||||
controller: _headerKeyController,
|
||||
validator: (value) => _validateHeaderKey(value ?? ''),
|
||||
@@ -624,8 +625,8 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: AccessibleFormField(
|
||||
label: 'Header Value',
|
||||
hint: 'api-key-123 or Bearer token',
|
||||
label: AppLocalizations.of(context)!.headerValue,
|
||||
hint: AppLocalizations.of(context)!.headerValueHint,
|
||||
controller: _headerValueController,
|
||||
validator: (value) => _validateHeaderValue(value ?? ''),
|
||||
semanticLabel: 'Enter header value',
|
||||
@@ -638,8 +639,8 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
|
||||
icon: Platform.isIOS ? CupertinoIcons.plus : Icons.add,
|
||||
onPressed: _customHeaders.length >= 10 ? null : _addCustomHeader,
|
||||
tooltip: _customHeaders.length >= 10
|
||||
? 'Maximum headers reached'
|
||||
: 'Add header',
|
||||
? AppLocalizations.of(context)!.maximumHeadersReached
|
||||
: AppLocalizations.of(context)!.addHeader,
|
||||
backgroundColor: _customHeaders.length >= 10
|
||||
? context.conduitTheme.surfaceContainer
|
||||
: context.conduitTheme.buttonPrimary,
|
||||
@@ -694,7 +695,7 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
|
||||
ConduitIconButton(
|
||||
icon: Platform.isIOS ? CupertinoIcons.xmark : Icons.close,
|
||||
onPressed: () => _removeCustomHeader(entry.key),
|
||||
tooltip: 'Remove header',
|
||||
tooltip: AppLocalizations.of(context)!.removeHeader,
|
||||
backgroundColor: context.conduitTheme.error.withValues(alpha: 0.1),
|
||||
iconColor: context.conduitTheme.error,
|
||||
isCompact: true,
|
||||
@@ -710,7 +711,9 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: Spacing.lg),
|
||||
child: ConduitButton(
|
||||
text: _isConnecting ? 'Connecting...' : 'Connect to Server',
|
||||
text: _isConnecting
|
||||
? AppLocalizations.of(context)!.connecting
|
||||
: AppLocalizations.of(context)!.connectToServerButton,
|
||||
icon: _isConnecting
|
||||
? null
|
||||
: (Platform.isIOS ? CupertinoIcons.arrow_right : Icons.arrow_forward),
|
||||
@@ -866,4 +869,4 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
|
||||
});
|
||||
HapticFeedback.lightImpact();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import '../../../shared/theme/theme_extensions.dart';
|
||||
import '../../../shared/widgets/markdown/streaming_markdown_widget.dart';
|
||||
import '../../../core/utils/reasoning_parser.dart';
|
||||
import 'enhanced_image_attachment.dart';
|
||||
import 'package:conduit/l10n/app_localizations.dart';
|
||||
|
||||
class AssistantMessageWidget extends ConsumerStatefulWidget {
|
||||
final dynamic message;
|
||||
@@ -559,7 +560,7 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
||||
icon: Platform.isIOS
|
||||
? CupertinoIcons.arrow_clockwise
|
||||
: Icons.refresh,
|
||||
label: 'Retry',
|
||||
label: AppLocalizations.of(context)!.retry,
|
||||
onTap: widget.onRegenerate,
|
||||
),
|
||||
] else ...[
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:dio/dio.dart' as dio;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import '../../../shared/theme/theme_extensions.dart';
|
||||
import 'package:conduit/l10n/app_localizations.dart';
|
||||
import '../../../core/providers/app_providers.dart';
|
||||
import '../../auth/providers/unified_auth_providers.dart';
|
||||
|
||||
@@ -74,6 +75,7 @@ class _EnhancedImageAttachmentState
|
||||
}
|
||||
|
||||
Future<void> _loadImage() async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
// Check global cache first
|
||||
if (_globalImageCache.containsKey(widget.attachmentId)) {
|
||||
if (mounted) {
|
||||
@@ -142,7 +144,7 @@ class _EnhancedImageAttachmentState
|
||||
return;
|
||||
} else {
|
||||
// If API service is not available, show error
|
||||
final error = 'Unable to load image: API service not available';
|
||||
final error = l10n.unableToLoadImage;
|
||||
_globalErrorStates[widget.attachmentId] = error;
|
||||
_globalLoadingStates[widget.attachmentId] = false;
|
||||
if (mounted) {
|
||||
@@ -157,7 +159,7 @@ class _EnhancedImageAttachmentState
|
||||
|
||||
final api = ref.read(apiServiceProvider);
|
||||
if (api == null) {
|
||||
final error = 'API service not available';
|
||||
final error = l10n.apiUnavailable;
|
||||
_globalErrorStates[widget.attachmentId] = error;
|
||||
_globalLoadingStates[widget.attachmentId] = false;
|
||||
if (mounted) {
|
||||
@@ -176,7 +178,7 @@ class _EnhancedImageAttachmentState
|
||||
final ext = fileName.toLowerCase().split('.').last;
|
||||
|
||||
if (!['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].contains(ext)) {
|
||||
final error = 'Not an image file: $fileName';
|
||||
final error = l10n.notAnImageFile(fileName);
|
||||
_globalErrorStates[widget.attachmentId] = error;
|
||||
_globalLoadingStates[widget.attachmentId] = false;
|
||||
if (mounted) {
|
||||
@@ -214,7 +216,7 @@ class _EnhancedImageAttachmentState
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
final error = 'Failed to load image: ${e.toString()}';
|
||||
final error = l10n.failedToLoadImage(e.toString());
|
||||
_globalErrorStates[widget.attachmentId] = error;
|
||||
_globalLoadingStates[widget.attachmentId] = false;
|
||||
if (mounted) {
|
||||
@@ -443,7 +445,7 @@ class _EnhancedImageAttachmentState
|
||||
if (commaIndex != -1) {
|
||||
actualBase64 = _cachedImageData!.substring(commaIndex + 1);
|
||||
} else {
|
||||
throw Exception('Invalid data URL format');
|
||||
throw Exception(AppLocalizations.of(context)!.invalidDataUrl);
|
||||
}
|
||||
} else {
|
||||
actualBase64 = _cachedImageData!;
|
||||
@@ -456,14 +458,14 @@ class _EnhancedImageAttachmentState
|
||||
fit: BoxFit.cover,
|
||||
gaplessPlayback: true, // Prevents flashing during rebuilds
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
_errorMessage = 'Failed to decode image';
|
||||
_errorMessage = AppLocalizations.of(context)!.failedToDecodeImage;
|
||||
return _buildErrorState();
|
||||
},
|
||||
);
|
||||
|
||||
return _wrapImage(imageWidget);
|
||||
} catch (e) {
|
||||
_errorMessage = 'Invalid image format';
|
||||
_errorMessage = AppLocalizations.of(context)!.invalidImageFormat;
|
||||
return _buildErrorState();
|
||||
}
|
||||
}
|
||||
@@ -650,6 +652,7 @@ class FullScreenImageViewer extends ConsumerWidget {
|
||||
}
|
||||
|
||||
Future<void> _shareImage(BuildContext context, WidgetRef ref) async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
try {
|
||||
Uint8List bytes;
|
||||
String? fileExtension;
|
||||
@@ -679,7 +682,7 @@ class FullScreenImageViewer extends ConsumerWidget {
|
||||
);
|
||||
final data = response.data;
|
||||
if (data == null || data.isEmpty) {
|
||||
throw Exception('Empty image data');
|
||||
throw Exception(l10n.emptyImageData);
|
||||
}
|
||||
bytes = Uint8List.fromList(data);
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import '../../tools/widgets/unified_tools_modal.dart';
|
||||
import '../../tools/providers/tools_providers.dart';
|
||||
|
||||
import '../../../shared/utils/platform_utils.dart';
|
||||
import 'package:conduit/l10n/app_localizations.dart';
|
||||
|
||||
class ModernChatInput extends ConsumerStatefulWidget {
|
||||
final Function(String) onSendMessage;
|
||||
@@ -264,7 +265,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
onTap: widget.enabled
|
||||
? _showAttachmentOptions
|
||||
: null,
|
||||
tooltip: 'Add attachment',
|
||||
tooltip: AppLocalizations.of(context)!.addAttachment,
|
||||
),
|
||||
const SizedBox(width: Spacing.sm),
|
||||
],
|
||||
@@ -272,8 +273,8 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
Expanded(
|
||||
child: Semantics(
|
||||
textField: true,
|
||||
label: 'Message input',
|
||||
hint: 'Type your message',
|
||||
label: AppLocalizations.of(context)!.messageInputLabel,
|
||||
hint: AppLocalizations.of(context)!.messageInputHint,
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
@@ -291,7 +292,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
color: context.conduitTheme.inputText,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Message...',
|
||||
hintText: AppLocalizations.of(context)!.messageHintText,
|
||||
hintStyle: TextStyle(
|
||||
color:
|
||||
context.conduitTheme.inputPlaceholder,
|
||||
@@ -363,7 +364,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
onTap: widget.enabled
|
||||
? _showAttachmentOptions
|
||||
: null,
|
||||
tooltip: 'Add attachment',
|
||||
tooltip: AppLocalizations.of(context)!.addAttachment,
|
||||
),
|
||||
const SizedBox(width: Spacing.sm),
|
||||
// Tools button
|
||||
@@ -374,7 +375,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
_showUnifiedToolsModal();
|
||||
}
|
||||
: null,
|
||||
tooltip: 'Tools',
|
||||
tooltip: AppLocalizations.of(context)!.tools,
|
||||
isActive:
|
||||
ref
|
||||
.watch(selectedToolIdsProvider)
|
||||
@@ -391,7 +392,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
onTap: widget.enabled
|
||||
? widget.onVoiceInput
|
||||
: null,
|
||||
tooltip: 'Voice input',
|
||||
tooltip: AppLocalizations.of(context)!.voiceInput,
|
||||
isActive: _isRecording,
|
||||
),
|
||||
const SizedBox(width: Spacing.sm),
|
||||
@@ -431,7 +432,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
// Generating -> STOP variant
|
||||
if (isGenerating) {
|
||||
return Tooltip(
|
||||
message: 'Stop generating',
|
||||
message: AppLocalizations.of(context)!.stopGenerating,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
@@ -482,7 +483,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
|
||||
// Default SEND variant
|
||||
return Tooltip(
|
||||
message: enabled ? 'Send message' : 'Send',
|
||||
message: enabled ? AppLocalizations.of(context)!.sendMessage : AppLocalizations.of(context)!.send,
|
||||
child: Opacity(
|
||||
opacity: enabled ? Alpha.primary : Alpha.disabled,
|
||||
child: IgnorePointer(
|
||||
@@ -626,7 +627,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
Expanded(
|
||||
child: _buildAttachmentOption(
|
||||
icon: Platform.isIOS ? CupertinoIcons.doc : Icons.attach_file,
|
||||
label: 'File',
|
||||
label: AppLocalizations.of(context)!.file,
|
||||
onTap: () {
|
||||
HapticFeedback.lightImpact();
|
||||
Navigator.pop(context); // Close modal
|
||||
@@ -637,7 +638,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
Expanded(
|
||||
child: _buildAttachmentOption(
|
||||
icon: Platform.isIOS ? CupertinoIcons.photo : Icons.image,
|
||||
label: 'Photo',
|
||||
label: AppLocalizations.of(context)!.photo,
|
||||
onTap: () {
|
||||
HapticFeedback.lightImpact();
|
||||
Navigator.pop(context); // Close modal
|
||||
@@ -650,7 +651,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
icon: Platform.isIOS
|
||||
? CupertinoIcons.camera
|
||||
: Icons.camera_alt,
|
||||
label: 'Camera',
|
||||
label: AppLocalizations.of(context)!.camera,
|
||||
onTap: () {
|
||||
HapticFeedback.lightImpact();
|
||||
Navigator.pop(context); // Close modal
|
||||
|
||||
@@ -9,6 +9,7 @@ import '../../../shared/widgets/improved_loading_states.dart';
|
||||
|
||||
import '../../../shared/utils/ui_utils.dart';
|
||||
import '../../../shared/widgets/sheet_handle.dart';
|
||||
import 'package:conduit/l10n/app_localizations.dart';
|
||||
|
||||
/// Files page for managing documents and uploads
|
||||
class WorkspacePage extends ConsumerStatefulWidget {
|
||||
@@ -108,10 +109,10 @@ class _WorkspacePageState extends ConsumerState<WorkspacePage>
|
||||
size: IconSize.button,
|
||||
),
|
||||
onPressed: () => NavigationService.goBack(),
|
||||
tooltip: 'Back',
|
||||
tooltip: AppLocalizations.of(context)!.back,
|
||||
),
|
||||
title: Text(
|
||||
'Workspace',
|
||||
AppLocalizations.of(context)!.workspace,
|
||||
style: AppTypography.headlineSmallStyle.copyWith(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -162,7 +163,7 @@ class _WorkspacePageState extends ConsumerState<WorkspacePage>
|
||||
Expanded(
|
||||
child: _buildTabButton(
|
||||
index: 0,
|
||||
label: 'Recent Files',
|
||||
label: AppLocalizations.of(context)!.recentFiles,
|
||||
isSelected: _selectedTab == 0,
|
||||
),
|
||||
),
|
||||
@@ -170,7 +171,7 @@ class _WorkspacePageState extends ConsumerState<WorkspacePage>
|
||||
Expanded(
|
||||
child: _buildTabButton(
|
||||
index: 1,
|
||||
label: 'Knowledge Base',
|
||||
label: AppLocalizations.of(context)!.knowledgeBase,
|
||||
isSelected: _selectedTab == 1,
|
||||
),
|
||||
),
|
||||
@@ -229,11 +230,10 @@ class _WorkspacePageState extends ConsumerState<WorkspacePage>
|
||||
ios: CupertinoIcons.doc,
|
||||
android: Icons.description_outlined,
|
||||
),
|
||||
title: 'No files yet',
|
||||
subtitle:
|
||||
'Upload documents to reference in your conversations with Conduit',
|
||||
title: AppLocalizations.of(context)!.noFilesYet,
|
||||
subtitle: AppLocalizations.of(context)!.uploadDocsPrompt,
|
||||
onAction: _showUploadOptions,
|
||||
actionLabel: 'Upload your first file',
|
||||
actionLabel: AppLocalizations.of(context)!.uploadFirstFile,
|
||||
showAnimation: true,
|
||||
),
|
||||
).animate().fadeIn(
|
||||
@@ -251,8 +251,8 @@ class _WorkspacePageState extends ConsumerState<WorkspacePage>
|
||||
ios: CupertinoIcons.book,
|
||||
android: Icons.library_books,
|
||||
),
|
||||
title: 'Knowledge base is empty',
|
||||
subtitle: 'Create collections of related documents for easy reference',
|
||||
title: AppLocalizations.of(context)!.knowledgeBaseEmpty,
|
||||
subtitle: AppLocalizations.of(context)!.createCollectionsPrompt,
|
||||
onAction: _showKnowledgeBaseOptions,
|
||||
actionLabel: 'Create knowledge base',
|
||||
showAnimation: true,
|
||||
@@ -293,7 +293,7 @@ class _WorkspacePageState extends ConsumerState<WorkspacePage>
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(Spacing.modalPadding),
|
||||
child: Text(
|
||||
'Upload File',
|
||||
AppLocalizations.of(context)!.uploadFileTitle,
|
||||
style: context.conduitTheme.headingSmall?.copyWith(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -307,8 +307,8 @@ class _WorkspacePageState extends ConsumerState<WorkspacePage>
|
||||
ios: CupertinoIcons.camera,
|
||||
android: Icons.camera_alt,
|
||||
),
|
||||
title: 'Take Photo',
|
||||
subtitle: 'Capture a document or image',
|
||||
title: AppLocalizations.of(context)!.takePhoto,
|
||||
subtitle: AppLocalizations.of(context)!.captureDocumentOrImage,
|
||||
onTap: () => _handleUploadOption('camera'),
|
||||
),
|
||||
_buildUploadOption(
|
||||
@@ -316,8 +316,8 @@ class _WorkspacePageState extends ConsumerState<WorkspacePage>
|
||||
ios: CupertinoIcons.photo,
|
||||
android: Icons.photo_library,
|
||||
),
|
||||
title: 'Photo Library',
|
||||
subtitle: 'Choose from your photos',
|
||||
title: AppLocalizations.of(context)!.chooseFromGallery,
|
||||
subtitle: AppLocalizations.of(context)!.chooseFromGallery,
|
||||
onTap: () => _handleUploadOption('gallery'),
|
||||
),
|
||||
_buildUploadOption(
|
||||
@@ -325,8 +325,8 @@ class _WorkspacePageState extends ConsumerState<WorkspacePage>
|
||||
ios: CupertinoIcons.doc,
|
||||
android: Icons.description,
|
||||
),
|
||||
title: 'Document',
|
||||
subtitle: 'PDF, Word, or text file',
|
||||
title: AppLocalizations.of(context)!.document,
|
||||
subtitle: AppLocalizations.of(context)!.documentHint,
|
||||
onTap: () => _handleUploadOption('document'),
|
||||
),
|
||||
|
||||
@@ -430,10 +430,16 @@ class _WorkspacePageState extends ConsumerState<WorkspacePage>
|
||||
|
||||
void _handleUploadOption(String type) {
|
||||
NavigationService.goBack();
|
||||
UiUtils.showMessage(context, 'File upload for $type is coming soon!');
|
||||
UiUtils.showMessage(
|
||||
context,
|
||||
AppLocalizations.of(context)!.fileUploadComingSoon(type),
|
||||
);
|
||||
}
|
||||
|
||||
void _showKnowledgeBaseOptions() {
|
||||
UiUtils.showMessage(context, 'Knowledge base creation is coming soon!');
|
||||
UiUtils.showMessage(
|
||||
context,
|
||||
AppLocalizations.of(context)!.kbCreationComingSoon,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/cupertino.dart';
|
||||
import '../../../shared/theme/theme_extensions.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import '../../../shared/widgets/sheet_handle.dart';
|
||||
import 'package:conduit/l10n/app_localizations.dart';
|
||||
|
||||
class OnboardingSheet extends StatefulWidget {
|
||||
const OnboardingSheet({super.key});
|
||||
@@ -14,44 +15,39 @@ class OnboardingSheet extends StatefulWidget {
|
||||
class _OnboardingSheetState extends State<OnboardingSheet> {
|
||||
final PageController _controller = PageController();
|
||||
int _index = 0;
|
||||
late List<_OnboardingPage> _pages;
|
||||
|
||||
final List<_OnboardingPage> _pages = const [
|
||||
_OnboardingPage(
|
||||
title: 'Start a conversation',
|
||||
subtitle:
|
||||
'Choose a model, then type below to begin. Tap New Chat anytime.',
|
||||
icon: CupertinoIcons.chat_bubble_2,
|
||||
bullets: [
|
||||
'Tap the model name in the top bar to switch models',
|
||||
'Use New Chat to reset context',
|
||||
],
|
||||
),
|
||||
_OnboardingPage(
|
||||
title: 'Attach context',
|
||||
subtitle: 'Ground responses by adding files or images.',
|
||||
icon: CupertinoIcons.doc_on_doc,
|
||||
bullets: ['Files: PDFs, docs, datasets', 'Images: photos or screenshots'],
|
||||
),
|
||||
_OnboardingPage(
|
||||
title: 'Speak naturally',
|
||||
subtitle: 'Tap the mic to dictate with live waveform feedback.',
|
||||
icon: CupertinoIcons.mic_fill,
|
||||
bullets: [
|
||||
'Stop anytime; partial text is preserved',
|
||||
'Great for quick notes or long prompts',
|
||||
],
|
||||
),
|
||||
_OnboardingPage(
|
||||
title: 'Quick actions',
|
||||
subtitle:
|
||||
'Use the top‑left menu to open the chats list and navigation.',
|
||||
icon: CupertinoIcons.line_horizontal_3,
|
||||
bullets: [
|
||||
'Tap the menu to open the chats list and navigation',
|
||||
'Jump instantly to New Chat, Files, or Profile',
|
||||
],
|
||||
),
|
||||
];
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
_pages = [
|
||||
_OnboardingPage(
|
||||
title: l10n.onboardStartTitle,
|
||||
subtitle: l10n.onboardStartSubtitle,
|
||||
icon: CupertinoIcons.chat_bubble_2,
|
||||
bullets: [l10n.onboardStartBullet1, l10n.onboardStartBullet2],
|
||||
),
|
||||
_OnboardingPage(
|
||||
title: l10n.onboardAttachTitle,
|
||||
subtitle: l10n.onboardAttachSubtitle,
|
||||
icon: CupertinoIcons.doc_on_doc,
|
||||
bullets: [l10n.onboardAttachBullet1, l10n.onboardAttachBullet2],
|
||||
),
|
||||
_OnboardingPage(
|
||||
title: l10n.onboardSpeakTitle,
|
||||
subtitle: l10n.onboardSpeakSubtitle,
|
||||
icon: CupertinoIcons.mic_fill,
|
||||
bullets: [l10n.onboardSpeakBullet1, l10n.onboardSpeakBullet2],
|
||||
),
|
||||
_OnboardingPage(
|
||||
title: l10n.onboardQuickTitle,
|
||||
subtitle: l10n.onboardQuickSubtitle,
|
||||
icon: CupertinoIcons.line_horizontal_3,
|
||||
bullets: [l10n.onboardQuickBullet1, l10n.onboardQuickBullet2],
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
void _next() {
|
||||
if (_index < _pages.length - 1) {
|
||||
@@ -133,7 +129,7 @@ class _OnboardingSheetState extends State<OnboardingSheet> {
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(
|
||||
'Skip',
|
||||
AppLocalizations.of(context)!.skip,
|
||||
style: TextStyle(
|
||||
color: context.conduitTheme.textSecondary,
|
||||
),
|
||||
@@ -155,7 +151,11 @@ class _OnboardingSheetState extends State<OnboardingSheet> {
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(_index == _pages.length - 1 ? 'Done' : 'Next'),
|
||||
child: Text(
|
||||
_index == _pages.length - 1
|
||||
? AppLocalizations.of(context)!.done
|
||||
: AppLocalizations.of(context)!.next,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:conduit/l10n/app_localizations.dart';
|
||||
import '../../../core/widgets/error_boundary.dart';
|
||||
import '../../../shared/widgets/improved_loading_states.dart';
|
||||
|
||||
@@ -46,12 +47,12 @@ class ProfilePage extends ConsumerWidget {
|
||||
color: context.conduitTheme.textPrimary,
|
||||
),
|
||||
onPressed: () => Navigator.of(context).maybePop(),
|
||||
tooltip: 'Back',
|
||||
tooltip: AppLocalizations.of(context)!.back,
|
||||
),
|
||||
toolbarHeight: kToolbarHeight,
|
||||
titleSpacing: 0.0,
|
||||
title: Text(
|
||||
'You',
|
||||
AppLocalizations.of(context)!.you,
|
||||
style: AppTypography.headlineSmallStyle.copyWith(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -106,10 +107,10 @@ class ProfilePage extends ConsumerWidget {
|
||||
color: context.conduitTheme.textPrimary,
|
||||
),
|
||||
onPressed: () => Navigator.of(context).maybePop(),
|
||||
tooltip: 'Back',
|
||||
tooltip: AppLocalizations.of(context)!.back,
|
||||
),
|
||||
title: Text(
|
||||
'You',
|
||||
AppLocalizations.of(context)!.you,
|
||||
style: AppTypography.headlineSmallStyle.copyWith(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -117,8 +118,8 @@ class ProfilePage extends ConsumerWidget {
|
||||
),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: const Center(
|
||||
child: ImprovedLoadingState(message: 'Loading profile...'),
|
||||
body: Center(
|
||||
child: ImprovedLoadingState(message: AppLocalizations.of(context)!.loadingProfile),
|
||||
),
|
||||
),
|
||||
error: (error, stack) => Scaffold(
|
||||
@@ -136,10 +137,10 @@ class ProfilePage extends ConsumerWidget {
|
||||
color: context.conduitTheme.textPrimary,
|
||||
),
|
||||
onPressed: () => Navigator.of(context).maybePop(),
|
||||
tooltip: 'Back',
|
||||
tooltip: AppLocalizations.of(context)!.back,
|
||||
),
|
||||
title: Text(
|
||||
'You',
|
||||
AppLocalizations.of(context)!.you,
|
||||
style: AppTypography.headlineSmallStyle.copyWith(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -149,8 +150,8 @@ class ProfilePage extends ConsumerWidget {
|
||||
),
|
||||
body: Center(
|
||||
child: ImprovedEmptyState(
|
||||
title: 'Unable to load profile',
|
||||
subtitle: 'Please check your connection and try again',
|
||||
title: AppLocalizations.of(context)!.unableToLoadProfile,
|
||||
subtitle: AppLocalizations.of(context)!.pleaseCheckConnection,
|
||||
icon: UiUtils.platformIcon(
|
||||
ios: CupertinoIcons.exclamationmark_triangle,
|
||||
android: Icons.error_outline,
|
||||
@@ -213,7 +214,7 @@ class ProfilePage extends ConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Account',
|
||||
AppLocalizations.of(context)!.account,
|
||||
style: context.conduitTheme.headingSmall?.copyWith(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
),
|
||||
@@ -227,6 +228,8 @@ class ProfilePage extends ConsumerWidget {
|
||||
Divider(color: context.conduitTheme.dividerColor, height: 1),
|
||||
_buildThemeToggleTile(context, ref),
|
||||
Divider(color: context.conduitTheme.dividerColor, height: 1),
|
||||
_buildLanguageTile(context, ref),
|
||||
Divider(color: context.conduitTheme.dividerColor, height: 1),
|
||||
_buildAboutTile(context),
|
||||
Divider(color: context.conduitTheme.dividerColor, height: 1),
|
||||
_buildAccountOption(
|
||||
@@ -234,8 +237,8 @@ class ProfilePage extends ConsumerWidget {
|
||||
ios: CupertinoIcons.square_arrow_left,
|
||||
android: Icons.logout,
|
||||
),
|
||||
title: 'Sign Out',
|
||||
subtitle: 'End your session',
|
||||
title: AppLocalizations.of(context)!.signOut,
|
||||
subtitle: AppLocalizations.of(context)!.endYourSession,
|
||||
onTap: () => _signOut(context, ref),
|
||||
isDestructive: true,
|
||||
),
|
||||
@@ -342,14 +345,14 @@ class ProfilePage extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
'Default Model',
|
||||
AppLocalizations.of(context)!.defaultModel,
|
||||
style: context.conduitTheme.bodyLarge?.copyWith(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
settings.defaultModel != null ? currentModel.name : 'Auto-select',
|
||||
settings.defaultModel != null ? currentModel.name : AppLocalizations.of(context)!.autoSelect,
|
||||
style: context.conduitTheme.bodySmall?.copyWith(
|
||||
color: context.conduitTheme.textSecondary,
|
||||
),
|
||||
@@ -388,14 +391,14 @@ class ProfilePage extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
'Default Model',
|
||||
AppLocalizations.of(context)!.defaultModel,
|
||||
style: context.conduitTheme.bodyLarge?.copyWith(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'Loading models...',
|
||||
AppLocalizations.of(context)!.loadingModels,
|
||||
style: context.conduitTheme.bodySmall?.copyWith(
|
||||
color: context.conduitTheme.textSecondary,
|
||||
),
|
||||
@@ -424,14 +427,14 @@ class ProfilePage extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
'Default Model',
|
||||
AppLocalizations.of(context)!.defaultModel,
|
||||
style: context.conduitTheme.bodyLarge?.copyWith(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'Failed to load models',
|
||||
AppLocalizations.of(context)!.failedToLoadModels,
|
||||
style: context.conduitTheme.bodySmall?.copyWith(
|
||||
color: context.conduitTheme.error,
|
||||
),
|
||||
@@ -440,6 +443,132 @@ class ProfilePage extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLanguageTile(BuildContext context, WidgetRef ref) {
|
||||
final locale = ref.watch(localeProvider);
|
||||
final currentCode = locale?.languageCode ?? 'system';
|
||||
final label = () {
|
||||
switch (currentCode) {
|
||||
case 'en':
|
||||
return 'English';
|
||||
case 'de':
|
||||
return 'Deutsch';
|
||||
case 'fr':
|
||||
return 'Français';
|
||||
case 'it':
|
||||
return 'Italiano';
|
||||
default:
|
||||
return 'System';
|
||||
}
|
||||
}();
|
||||
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.listItemPadding,
|
||||
vertical: Spacing.sm,
|
||||
),
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.all(Spacing.sm),
|
||||
decoration: BoxDecoration(
|
||||
color: context.conduitTheme.buttonPrimary.withValues(
|
||||
alpha: Alpha.highlight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.small),
|
||||
),
|
||||
child: Icon(
|
||||
UiUtils.platformIcon(
|
||||
ios: CupertinoIcons.globe,
|
||||
android: Icons.language,
|
||||
),
|
||||
color: context.conduitTheme.buttonPrimary,
|
||||
size: IconSize.medium,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!.menuItem,
|
||||
style: context.conduitTheme.bodyLarge?.copyWith(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
label,
|
||||
style: context.conduitTheme.bodySmall?.copyWith(
|
||||
color: context.conduitTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
trailing: Icon(
|
||||
UiUtils.platformIcon(
|
||||
ios: CupertinoIcons.chevron_right,
|
||||
android: Icons.chevron_right,
|
||||
),
|
||||
color: context.conduitTheme.iconSecondary,
|
||||
size: IconSize.small,
|
||||
),
|
||||
onTap: () async {
|
||||
final selected = await _showLanguageSelector(context, currentCode);
|
||||
if (selected != null) {
|
||||
if (selected == 'system') {
|
||||
await ref.read(localeProvider.notifier).setLocale(null);
|
||||
} else {
|
||||
await ref.read(localeProvider.notifier).setLocale(Locale(selected));
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<String?> _showLanguageSelector(BuildContext context, String current) {
|
||||
return showModalBottomSheet<String>(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: context.conduitTheme.surfaceBackground,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(AppBorderRadius.modal),
|
||||
),
|
||||
boxShadow: ConduitShadows.modal,
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: Spacing.sm),
|
||||
ListTile(
|
||||
title: const Text('System'),
|
||||
trailing: current == 'system' ? const Icon(Icons.check) : null,
|
||||
onTap: () => Navigator.pop(context, 'system'),
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('English'),
|
||||
trailing: current == 'en' ? const Icon(Icons.check) : null,
|
||||
onTap: () => Navigator.pop(context, 'en'),
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Deutsch'),
|
||||
trailing: current == 'de' ? const Icon(Icons.check) : null,
|
||||
onTap: () => Navigator.pop(context, 'de'),
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Français'),
|
||||
trailing: current == 'fr' ? const Icon(Icons.check) : null,
|
||||
onTap: () => Navigator.pop(context, 'fr'),
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Italiano'),
|
||||
trailing: current == 'it' ? const Icon(Icons.check) : null,
|
||||
onTap: () => Navigator.pop(context, 'it'),
|
||||
),
|
||||
const SizedBox(height: Spacing.sm),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildThemeToggleTile(BuildContext context, WidgetRef ref) {
|
||||
final themeMode = ref.watch(themeModeProvider);
|
||||
final platformBrightness = MediaQuery.platformBrightnessOf(context);
|
||||
@@ -579,7 +708,7 @@ class ProfilePage extends ConsumerWidget {
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: const Text('Close'),
|
||||
child: Text(AppLocalizations.of(ctx)!.closeButtonSemantic),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -614,9 +743,9 @@ class ProfilePage extends ConsumerWidget {
|
||||
void _signOut(BuildContext context, WidgetRef ref) async {
|
||||
final confirm = await UiUtils.showConfirmationDialog(
|
||||
context,
|
||||
title: 'Sign out?',
|
||||
message: 'You\'ll need to sign in again to continue',
|
||||
confirmText: 'Sign out',
|
||||
title: AppLocalizations.of(context)!.signOut,
|
||||
message: AppLocalizations.of(context)!.endYourSession,
|
||||
confirmText: AppLocalizations.of(context)!.signOut,
|
||||
isDestructive: true,
|
||||
);
|
||||
|
||||
@@ -756,7 +885,7 @@ class _DefaultModelBottomSheetState extends ConsumerState<_DefaultModelBottomShe
|
||||
controller: _searchController,
|
||||
style: TextStyle(color: context.conduitTheme.textPrimary),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search models...',
|
||||
hintText: AppLocalizations.of(context)!.searchModels,
|
||||
hintStyle: TextStyle(
|
||||
color: context.conduitTheme.inputPlaceholder,
|
||||
),
|
||||
@@ -799,7 +928,7 @@ class _DefaultModelBottomSheetState extends ConsumerState<_DefaultModelBottomShe
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'Available Models',
|
||||
AppLocalizations.of(context)!.availableModels,
|
||||
style: AppTypography.bodySmallStyle.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.conduitTheme.textSecondary,
|
||||
@@ -848,7 +977,7 @@ class _DefaultModelBottomSheetState extends ConsumerState<_DefaultModelBottomShe
|
||||
),
|
||||
const SizedBox(height: Spacing.md),
|
||||
Text(
|
||||
'No results',
|
||||
AppLocalizations.of(context)!.noResults,
|
||||
style: TextStyle(
|
||||
color: context.conduitTheme.textSecondary,
|
||||
fontSize: AppTypography.bodyLarge,
|
||||
@@ -958,7 +1087,7 @@ class _DefaultModelBottomSheetState extends ConsumerState<_DefaultModelBottomShe
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
isAutoSelect ? 'Auto-select' : model.name,
|
||||
isAutoSelect ? AppLocalizations.of(context)!.autoSelect : model.name,
|
||||
style: TextStyle(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
|
||||
Reference in New Issue
Block a user