diff --git a/lib/core/services/share_receiver_service.dart b/lib/core/services/share_receiver_service.dart index c381285..0935a9e 100644 --- a/lib/core/services/share_receiver_service.dart +++ b/lib/core/services/share_receiver_service.dart @@ -175,21 +175,26 @@ Future _processPayload(Ref ref, SharedPayload payload) async { final svc = ref.read(fileAttachmentServiceProvider); if (svc != null) { // Add files to attachment list and kick off uploads, mirroring UI flow - final files = payload.filePaths.map((p) => File(p)).toList(); - if (files.isNotEmpty) { - ref.read(attachedFilesProvider.notifier).addFiles(files); + final attachments = payload.filePaths + .map( + (p) => + LocalAttachment(file: File(p), displayName: path.basename(p)), + ) + .toList(); + if (attachments.isNotEmpty) { + ref.read(attachedFilesProvider.notifier).addFiles(attachments); // Enqueue uploads via task queue to unify progress + retry final activeConv = ref.read(activeConversationProvider); - for (final file in files) { + for (final attachment in attachments) { try { await ref .read(taskQueueProvider.notifier) .enqueueUploadMedia( conversationId: activeConv?.id, - filePath: file.path, - fileName: path.basename(file.path), - fileSize: await file.length(), + filePath: attachment.file.path, + fileName: attachment.displayName, + fileSize: await attachment.file.length(), ); } catch (_) {} } diff --git a/lib/features/chat/services/file_attachment_service.dart b/lib/features/chat/services/file_attachment_service.dart index d71a136..bf03d6b 100644 --- a/lib/features/chat/services/file_attachment_service.dart +++ b/lib/features/chat/services/file_attachment_service.dart @@ -10,6 +10,62 @@ import '../../../core/services/api_service.dart'; import '../../../core/providers/app_providers.dart'; import '../../../core/utils/debug_logger.dart'; +String _deriveDisplayName({ + required String? preferredName, + required String filePath, + String fallbackPrefix = 'attachment', +}) { + final String candidate = + (preferredName != null && preferredName.trim().isNotEmpty) + ? preferredName.trim() + : path.basename(filePath); + + final String pathExt = path.extension(filePath); + final String candidateExt = path.extension(candidate); + final String extension = (candidateExt.isNotEmpty ? candidateExt : pathExt) + .toLowerCase(); + + if (candidate.toLowerCase().startsWith('image_picker')) { + return _timestampedName(prefix: fallbackPrefix, extension: extension); + } + + if (candidate.isEmpty) { + return _timestampedName(prefix: fallbackPrefix, extension: extension); + } + + return candidate; +} + +String _timestampedName({required String prefix, required String extension}) { + final DateTime now = DateTime.now(); + String two(int value) => value.toString().padLeft(2, '0'); + final String ext = extension.isNotEmpty ? extension : '.jpg'; + final String timestamp = + '${now.year}${two(now.month)}${two(now.day)}_${two(now.hour)}${two(now.minute)}${two(now.second)}'; + return '${prefix}_$timestamp$ext'; +} + +/// Represents a locally selected attachment with a user-facing display name. +class LocalAttachment { + LocalAttachment({required this.file, required this.displayName}); + + final File file; + final String displayName; + + int get sizeInBytes => file.lengthSync(); + + String get extension { + final fromName = path.extension(displayName); + if (fromName.isNotEmpty) { + return fromName.toLowerCase(); + } + return path.extension(file.path).toLowerCase(); + } + + bool get isImage => + {'.jpg', '.jpeg', '.png', '.gif', '.webp'}.contains(extension); +} + class FileAttachmentService { final ApiService _apiService; final ImagePicker _imagePicker = ImagePicker(); @@ -17,7 +73,7 @@ class FileAttachmentService { FileAttachmentService(this._apiService); // Pick files from device - Future> pickFiles({ + Future> pickFiles({ bool allowMultiple = true, List? allowedExtensions, }) async { @@ -32,17 +88,51 @@ class FileAttachmentService { return []; } - return result.files - .where((file) => file.path != null) - .map((file) => File(file.path!)) - .toList(); + return result.files.where((file) => file.path != null).map((file) { + final displayName = _deriveDisplayName( + preferredName: file.name, + filePath: file.path!, + fallbackPrefix: 'attachment', + ); + return LocalAttachment( + file: File(file.path!), + displayName: displayName, + ); + }).toList(); } catch (e) { throw Exception('Failed to pick files: $e'); } } // Pick image from gallery - Future pickImage() async { + Future pickImage() async { + try { + final result = await FilePicker.platform.pickFiles( + allowMultiple: false, + type: FileType.image, + ); + + if (result != null && result.files.isNotEmpty) { + final platformFile = result.files.first; + if (platformFile.path != null) { + final displayName = _deriveDisplayName( + preferredName: platformFile.name, + filePath: platformFile.path!, + fallbackPrefix: 'photo', + ); + return LocalAttachment( + file: File(platformFile.path!), + displayName: displayName, + ); + } + } + } catch (e) { + DebugLogger.log( + 'FilePicker image failed: $e', + scope: 'attachments/image', + ); + } + try { final XFile? image = await _imagePicker.pickImage( source: ImageSource.gallery, @@ -50,14 +140,20 @@ class FileAttachmentService { ); if (image == null) return null; - return File(image.path); + final file = File(image.path); + final displayName = _deriveDisplayName( + preferredName: image.name, + filePath: image.path, + fallbackPrefix: 'photo', + ); + return LocalAttachment(file: file, displayName: displayName); } catch (e) { throw Exception('Failed to pick image: $e'); } } // Take photo from camera - Future takePhoto() async { + Future takePhoto() async { try { final XFile? photo = await _imagePicker.pickImage( source: ImageSource.camera, @@ -65,7 +161,13 @@ class FileAttachmentService { ); if (photo == null) return null; - return File(photo.path); + final file = File(photo.path); + final displayName = _deriveDisplayName( + preferredName: photo.name, + filePath: photo.path, + fallbackPrefix: 'photo', + ); + return LocalAttachment(file: file, displayName: displayName); } catch (e) { throw Exception('Failed to take photo: $e'); } @@ -199,15 +301,21 @@ class FileAttachmentService { } // Upload file with progress tracking - Stream uploadFile(File file) async* { + Stream uploadFile(LocalAttachment attachment) async* { DebugLogger.log( 'upload-start', scope: 'attachments/file', - data: {'path': file.path}, + data: { + 'path': attachment.file.path, + 'displayName': attachment.displayName, + }, ); try { - final fileName = path.basename(file.path); + final file = attachment.file; + final fileName = attachment.displayName; final fileSize = await file.length(); + final ext = path.extension(fileName).toLowerCase(); + final isImage = ['.jpg', '.jpeg', '.png', '.gif', '.webp'].contains(ext); DebugLogger.log( 'file-details', @@ -221,18 +329,9 @@ class FileAttachmentService { fileSize: fileSize, progress: 0.0, status: FileUploadStatus.uploading, + isImage: isImage, ); - // Check if this is an image file - final ext = path.extension(fileName).toLowerCase(); - final isImage = [ - 'jpg', - 'jpeg', - 'png', - 'gif', - 'webp', - ].contains(ext.substring(1)); - // Upload ALL files (including images) to server for consistency with web client DebugLogger.log('upload-progress', scope: 'attachments/file'); final fileId = await _apiService.uploadFile(file.path, fileName); @@ -253,8 +352,11 @@ class FileAttachmentService { ); } catch (e) { DebugLogger.error('upload-failed', scope: 'attachments/file', error: e); - final fileName = path.basename(file.path); + final file = attachment.file; + final fileName = attachment.displayName; final fileSize = await file.length(); + final ext = path.extension(fileName).toLowerCase(); + final isImage = ['.jpg', '.jpeg', '.png', '.gif', '.webp'].contains(ext); yield FileUploadState( file: file, @@ -263,18 +365,21 @@ class FileAttachmentService { progress: 0.0, status: FileUploadStatus.failed, error: e.toString(), + isImage: isImage, ); } } // Upload multiple files - Stream> uploadMultipleFiles(List files) async* { + Stream> uploadMultipleFiles( + List attachments, + ) async* { final states = {}; - for (final file in files) { - final uploadStream = uploadFile(file); + for (final attachment in attachments) { + final uploadStream = uploadFile(attachment); await for (final state in uploadStream) { - states[file.path] = state; + states[attachment.file.path] = state; yield states.values.toList(); } } @@ -386,8 +491,7 @@ enum FileUploadStatus { pending, uploading, completed, failed } class MockFileAttachmentService { final ImagePicker _imagePicker = ImagePicker(); - // Reuse the same methods from parent class - Future> pickFiles({ + Future> pickFiles({ bool allowMultiple = true, List? allowedExtensions, }) async { @@ -402,48 +506,73 @@ class MockFileAttachmentService { return []; } - return result.files - .where((file) => file.path != null) - .map((file) => File(file.path!)) - .toList(); + return result.files.where((file) => file.path != null).map((file) { + final displayName = _deriveDisplayName( + preferredName: file.name, + filePath: file.path!, + fallbackPrefix: 'attachment', + ); + return LocalAttachment( + file: File(file.path!), + displayName: displayName, + ); + }).toList(); } catch (e) { throw Exception('Failed to pick files: $e'); } } - Future pickImage() async { + Future pickImage() async { try { final XFile? image = await _imagePicker.pickImage( source: ImageSource.gallery, imageQuality: 85, ); - return image != null ? File(image.path) : null; + if (image == null) return null; + final file = File(image.path); + final displayName = _deriveDisplayName( + preferredName: image.name, + filePath: image.path, + fallbackPrefix: 'photo', + ); + return LocalAttachment(file: file, displayName: displayName); } catch (e) { throw Exception('Failed to pick image: $e'); } } - Future takePhoto() async { + Future takePhoto() async { try { final XFile? photo = await _imagePicker.pickImage( source: ImageSource.camera, imageQuality: 85, ); - return photo != null ? File(photo.path) : null; + if (photo == null) return null; + final file = File(photo.path); + final displayName = _deriveDisplayName( + preferredName: photo.name, + filePath: photo.path, + fallbackPrefix: 'photo', + ); + return LocalAttachment(file: file, displayName: displayName); } catch (e) { throw Exception('Failed to take photo: $e'); } } // Mock upload file with progress tracking - Stream uploadFile(File file) async* { + Stream uploadFile(LocalAttachment attachment) async* { DebugLogger.log( 'mock-upload', scope: 'attachments/mock', - data: {'path': file.path}, + data: { + 'path': attachment.file.path, + 'displayName': attachment.displayName, + }, ); - final fileName = path.basename(file.path); + final file = attachment.file; + final fileName = attachment.displayName; final fileSize = await file.length(); // Yield initial state @@ -453,6 +582,7 @@ class MockFileAttachmentService { fileSize: fileSize, progress: 0.0, status: FileUploadStatus.uploading, + isImage: attachment.isImage, ); // Simulate upload progress @@ -464,6 +594,7 @@ class MockFileAttachmentService { fileSize: fileSize, progress: i / 10, status: FileUploadStatus.uploading, + isImage: attachment.isImage, ); } @@ -475,28 +606,26 @@ class MockFileAttachmentService { progress: 1.0, status: FileUploadStatus.completed, fileId: 'mock_file_${DateTime.now().millisecondsSinceEpoch}', + isImage: attachment.isImage, ); DebugLogger.log('mock-complete', scope: 'attachments/mock'); } Future> uploadFiles( - List files, { + List attachments, { Function(int, int)? onProgress, required String conversationId, }) async { - // Simulate upload progress for reviewer mode final uploadIds = []; - for (int i = 0; i < files.length; i++) { + for (int i = 0; i < attachments.length; i++) { if (onProgress != null) { - // Simulate progress for (int j = 0; j <= 100; j += 10) { await Future.delayed(const Duration(milliseconds: 50)); onProgress(i, j); } } - // Generate mock upload ID uploadIds.add('mock_upload_${DateTime.now().millisecondsSinceEpoch}_$i'); } @@ -522,15 +651,16 @@ class AttachedFilesNotifier extends Notifier> { @override List build() => []; - void addFiles(List files) { - final newStates = files + void addFiles(List attachments) { + final newStates = attachments .map( - (file) => FileUploadState( - file: file, - fileName: path.basename(file.path), - fileSize: file.lengthSync(), + (attachment) => FileUploadState( + file: attachment.file, + fileName: attachment.displayName, + fileSize: attachment.sizeInBytes, progress: 0.0, status: FileUploadStatus.pending, + isImage: attachment.isImage, ), ) .toList(); diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index 106c84a..4043f48 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -26,7 +26,6 @@ import '../widgets/file_attachment_widget.dart'; import '../services/voice_input_service.dart'; import '../services/file_attachment_service.dart'; import 'voice_call_page.dart'; -import 'package:path/path.dart' as path; import '../../../shared/services/tasks/task_queue.dart'; import '../../tools/providers/tools_providers.dart'; import '../../../core/models/chat_message.dart'; @@ -390,19 +389,19 @@ class _ChatPageState extends ConsumerState { } try { - final files = await fileService.pickFiles(); - if (files.isEmpty) return; + final attachments = await fileService.pickFiles(); + if (attachments.isEmpty) return; // Validate file count final currentFiles = ref.read(attachedFilesProvider); - if (!validateFileCount(currentFiles.length, files.length, 10)) { + if (!validateFileCount(currentFiles.length, attachments.length, 10)) { if (!mounted) return; return; } // Validate file sizes - for (final file in files) { - final fileSize = await file.length(); + for (final attachment in attachments) { + final fileSize = await attachment.file.length(); if (!validateFileSize(fileSize, 20)) { if (!mounted) return; return; @@ -410,19 +409,19 @@ class _ChatPageState extends ConsumerState { } // Add files to the attachment list - ref.read(attachedFilesProvider.notifier).addFiles(files); + ref.read(attachedFilesProvider.notifier).addFiles(attachments); // Enqueue uploads via task queue for unified retry/progress final activeConv = ref.read(activeConversationProvider); - for (final file in files) { + for (final attachment in attachments) { try { await ref .read(taskQueueProvider.notifier) .enqueueUploadMedia( conversationId: activeConv?.id, - filePath: file.path, - fileName: path.basename(file.path), - fileSize: await file.length(), + filePath: attachment.file.path, + fileName: attachment.displayName, + fileSize: await attachment.file.length(), ); } catch (e) { if (!mounted) return; @@ -459,16 +458,23 @@ class _ChatPageState extends ConsumerState { try { DebugLogger.log('Picking image...', scope: 'chat/page'); - final image = fromCamera + final attachment = fromCamera ? await fileService.takePhoto() : await fileService.pickImage(); - if (image == null) { + if (attachment == null) { DebugLogger.log('No image selected', scope: 'chat/page'); return; } - DebugLogger.log('Image selected: ${image.path}', scope: 'chat/page'); - final imageSize = await image.length(); + DebugLogger.log( + 'Image selected: ${attachment.file.path}', + scope: 'chat/page', + ); + DebugLogger.log( + 'Image display name: ${attachment.displayName}', + scope: 'chat/page', + ); + final imageSize = await attachment.file.length(); DebugLogger.log('Image size: $imageSize bytes', scope: 'chat/page'); // Validate file size (default 20MB limit like OpenWebUI) @@ -485,7 +491,7 @@ class _ChatPageState extends ConsumerState { } // Add image to the attachment list - ref.read(attachedFilesProvider.notifier).addFiles([image]); + ref.read(attachedFilesProvider.notifier).addFiles([attachment]); DebugLogger.log('Image added to attachment list', scope: 'chat/page'); // Enqueue upload via task queue for unified retry/progress @@ -496,8 +502,8 @@ class _ChatPageState extends ConsumerState { .read(taskQueueProvider.notifier) .enqueueUploadMedia( conversationId: activeConv?.id, - filePath: image.path, - fileName: path.basename(image.path), + filePath: attachment.file.path, + fileName: attachment.displayName, fileSize: imageSize, ); } catch (e) { diff --git a/lib/features/chat/widgets/file_attachment_widget.dart b/lib/features/chat/widgets/file_attachment_widget.dart index c8d347c..c309fd6 100644 --- a/lib/features/chat/widgets/file_attachment_widget.dart +++ b/lib/features/chat/widgets/file_attachment_widget.dart @@ -2,11 +2,20 @@ import 'package:flutter/material.dart'; import '../../../shared/theme/theme_extensions.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'dart:io' show Platform; +import 'dart:io' show File, Platform; import 'package:conduit/l10n/app_localizations.dart'; import '../services/file_attachment_service.dart'; +import '../../../shared/services/tasks/task_queue.dart'; import '../../../shared/widgets/loading_states.dart'; +const Set _previewableImageExtensions = { + '.jpg', + '.jpeg', + '.png', + '.gif', + '.webp', +}; + class FileAttachmentWidget extends ConsumerWidget { const FileAttachmentWidget({super.key}); @@ -58,6 +67,9 @@ class _FileAttachmentCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final bool canPreview = _canPreviewImage(); + final Widget removeButton = _buildRemoveButton(context, ref); + return Container( width: 140, padding: const EdgeInsets.all(Spacing.sm), @@ -72,14 +84,19 @@ class _FileAttachmentCard extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Text(fileState.fileIcon, style: const TextStyle(fontSize: 20)), - const Spacer(), - _buildStatusIcon(context), - ], - ), - const SizedBox(height: Spacing.xs), + if (canPreview) ...[ + _buildImagePreview(context, removeButton), + const SizedBox(height: Spacing.sm), + ] else ...[ + Row( + children: [ + _buildStatusIcon(context), + const Spacer(), + removeButton, + ], + ), + const SizedBox(height: Spacing.xs), + ], Text( fileState.fileName, style: TextStyle( @@ -154,6 +171,50 @@ class _FileAttachmentCard extends ConsumerWidget { } } + Widget _buildRemoveButton(BuildContext context, WidgetRef ref) { + final String tooltip = MaterialLocalizations.of( + context, + ).deleteButtonTooltip; + return Tooltip( + message: tooltip, + child: Semantics( + button: true, + label: tooltip, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => _removeAttachment(ref), + child: Container( + width: 28, + height: 28, + alignment: Alignment.center, + decoration: BoxDecoration( + color: context.conduitTheme.cardBackground.withValues( + alpha: 0.85, + ), + shape: BoxShape.circle, + border: Border.all( + color: context.conduitTheme.cardBorder.withValues(alpha: 0.6), + width: BorderWidth.thin, + ), + ), + child: Icon( + Platform.isIOS ? CupertinoIcons.xmark : Icons.close, + size: 14, + color: context.conduitTheme.textPrimary.withValues(alpha: 0.8), + ), + ), + ), + ), + ); + } + + void _removeAttachment(WidgetRef ref) { + ref.read(attachedFilesProvider.notifier).removeFile(fileState.file.path); + ref + .read(taskQueueProvider.notifier) + .cancelUploadsForFile(fileState.file.path); + } + Widget _buildProgressBar(BuildContext context) { return ClipRRect( borderRadius: BorderRadius.circular(AppBorderRadius.xs), @@ -182,6 +243,88 @@ class _FileAttachmentCard extends ConsumerWidget { return context.conduitTheme.error.withValues(alpha: 0.3); } } + + bool _canPreviewImage() { + if (fileState.isImage != null) { + return fileState.isImage!; + } + final String lowerName = fileState.fileName.toLowerCase(); + return _previewableImageExtensions.any(lowerName.endsWith); + } + + Widget _buildImagePreview(BuildContext context, Widget removeButton) { + final File file = fileState.file; + final bool fileExists = file.existsSync(); + final Widget basePreview = fileExists + ? Image.file( + file, + fit: BoxFit.cover, + filterQuality: FilterQuality.medium, + errorBuilder: (context, error, stackTrace) => + _buildPreviewPlaceholderContent(context), + ) + : _buildPreviewPlaceholderContent(context); + + return DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(AppBorderRadius.xs), + border: Border.all( + color: context.conduitTheme.cardBorder, + width: BorderWidth.thin, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(AppBorderRadius.xs), + child: AspectRatio( + aspectRatio: 4 / 3, + child: Stack( + children: [ + Positioned.fill(child: basePreview), + if (fileState.status == FileUploadStatus.uploading) + Positioned.fill( + child: DecoratedBox( + decoration: BoxDecoration( + color: context.conduitTheme.cardBackground.withValues( + alpha: 0.35, + ), + ), + ), + ), + Positioned( + top: Spacing.xs, + right: Spacing.xs, + child: DecoratedBox( + decoration: BoxDecoration( + color: context.conduitTheme.cardBackground.withValues( + alpha: 0.85, + ), + borderRadius: BorderRadius.circular(AppBorderRadius.xs), + ), + child: Padding( + padding: const EdgeInsets.all(4), + child: _buildStatusIcon(context), + ), + ), + ), + Positioned( + top: Spacing.xs, + left: Spacing.xs, + child: removeButton, + ), + ], + ), + ), + ), + ); + } + + Widget _buildPreviewPlaceholderContent(BuildContext context) { + return Container( + color: context.conduitTheme.textPrimary.withValues(alpha: 0.08), + alignment: Alignment.center, + child: Text(fileState.fileIcon, style: const TextStyle(fontSize: 26)), + ); + } } // Attachment preview for messages diff --git a/lib/shared/services/tasks/task_queue.dart b/lib/shared/services/tasks/task_queue.dart index 2a2a041..78dec08 100644 --- a/lib/shared/services/tasks/task_queue.dart +++ b/lib/shared/services/tasks/task_queue.dart @@ -127,6 +127,43 @@ class TaskQueueNotifier extends Notifier> { await _save(); } + Future cancelUploadsForFile(String filePath) async { + bool updated = false; + state = [ + for (final task in state) + task.maybeMap( + uploadMedia: (upload) { + if ((upload.status == TaskStatus.queued || + upload.status == TaskStatus.running) && + upload.filePath == filePath) { + updated = true; + return upload.copyWith( + status: TaskStatus.cancelled, + completedAt: DateTime.now(), + ); + } + return upload; + }, + imageToDataUrl: (image) { + if ((image.status == TaskStatus.queued || + image.status == TaskStatus.running) && + image.filePath == filePath) { + updated = true; + return image.copyWith( + status: TaskStatus.cancelled, + completedAt: DateTime.now(), + ); + } + return image; + }, + orElse: () => task, + ), + ]; + if (updated) { + await _save(); + } + } + Future cancelByConversation(String conversationId) async { state = [ for (final t in state) diff --git a/lib/shared/services/tasks/task_worker.dart b/lib/shared/services/tasks/task_worker.dart index cf9e7fe..6f10e55 100644 --- a/lib/shared/services/tasks/task_worker.dart +++ b/lib/shared/services/tasks/task_worker.dart @@ -107,6 +107,10 @@ class TaskWorker { QueuedAttachmentStatus.failed => FileUploadStatus.failed, QueuedAttachmentStatus.cancelled => FileUploadStatus.failed, }; + const imageExts = {'.jpg', '.jpeg', '.png', '.gif', '.webp'}; + final lowerName = task.fileName.toLowerCase(); + final bool isImage = + existing.isImage ?? imageExts.any(lowerName.endsWith); final newState = FileUploadState( file: File(task.filePath), fileName: task.fileName, @@ -117,6 +121,7 @@ class TaskWorker { status: status, fileId: entry.fileId ?? existing.fileId, error: entry.lastError, + isImage: isImage, ); _ref .read(attachedFilesProvider.notifier) @@ -260,6 +265,7 @@ class TaskWorker { progress: 0.0, status: FileUploadStatus.uploading, fileId: existing.fileId, + isImage: existing.isImage ?? true, ); _ref .read(attachedFilesProvider.notifier) diff --git a/lib/shared/theme/tweakcn_themes.dart b/lib/shared/theme/tweakcn_themes.dart index f4bd4ff..3e50d20 100644 --- a/lib/shared/theme/tweakcn_themes.dart +++ b/lib/shared/theme/tweakcn_themes.dart @@ -482,7 +482,7 @@ class TweakcnThemes { preview: const [ Color(0xFFC96442), Color(0xFFE9E6DC), - Color(0xFF9C87F5), + Color(0xFF1A1915), ], ); @@ -508,7 +508,7 @@ class TweakcnThemes { preview: const [ Color(0xFFA1A1AA), Color(0xFFF4F4F5), - Color(0xFF2563EB), + Color(0xFF404040), ], ); diff --git a/pubspec.yaml b/pubspec.yaml index c2b2db9..bfbdc70 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -99,7 +99,7 @@ flutter: flutter_native_splash: # Splash background matches the light theme; `color_dark` handles dark mode. color: "#FFFFFF" - color_dark: "#0B0E14" + color_dark: "#0A0A0A" # Image to display on the splash screen image: assets/icons/icon.png @@ -107,7 +107,7 @@ flutter_native_splash: # Android specific settings android_12: color: "#FFFFFF" - color_dark: "#0B0E14" + color_dark: "#0A0A0A" # Web specific settings web: false