refactor: Enhance file attachment handling and UI components

- Updated the file attachment service to utilize a new LocalAttachment class, improving the management of file metadata such as display names.
- Refactored methods for picking and uploading files to accommodate the new LocalAttachment structure, ensuring consistent handling of file attributes.
- Improved the chat page to validate and manage file attachments more effectively, enhancing user experience during file uploads.
- Added functionality for image previews in the file attachment widget, allowing users to see selected images before sending.
- Introduced a remove button for attachments, improving usability by enabling users to easily discard unwanted files.
This commit is contained in:
cogwheel0
2025-10-19 13:50:54 +05:30
parent 1104661238
commit 2f8fd97022
8 changed files with 416 additions and 89 deletions

View File

@@ -175,21 +175,26 @@ Future<void> _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 (_) {}
}

View File

@@ -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 =>
<String>{'.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<List<File>> pickFiles({
Future<List<LocalAttachment>> pickFiles({
bool allowMultiple = true,
List<String>? 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<File?> pickImage() async {
Future<LocalAttachment?> 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<File?> takePhoto() async {
Future<LocalAttachment?> 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<FileUploadState> uploadFile(File file) async* {
Stream<FileUploadState> 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<List<FileUploadState>> uploadMultipleFiles(List<File> files) async* {
Stream<List<FileUploadState>> uploadMultipleFiles(
List<LocalAttachment> attachments,
) async* {
final states = <String, FileUploadState>{};
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<List<File>> pickFiles({
Future<List<LocalAttachment>> pickFiles({
bool allowMultiple = true,
List<String>? 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<File?> pickImage() async {
Future<LocalAttachment?> 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<File?> takePhoto() async {
Future<LocalAttachment?> 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<FileUploadState> uploadFile(File file) async* {
Stream<FileUploadState> 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<List<String>> uploadFiles(
List<File> files, {
List<LocalAttachment> attachments, {
Function(int, int)? onProgress,
required String conversationId,
}) async {
// Simulate upload progress for reviewer mode
final uploadIds = <String>[];
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<List<FileUploadState>> {
@override
List<FileUploadState> build() => [];
void addFiles(List<File> files) {
final newStates = files
void addFiles(List<LocalAttachment> 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();

View File

@@ -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<ChatPage> {
}
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<ChatPage> {
}
// 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<ChatPage> {
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<ChatPage> {
}
// 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<ChatPage> {
.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) {

View File

@@ -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<String> _previewableImageExtensions = <String>{
'.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: [
if (canPreview) ...[
_buildImagePreview(context, removeButton),
const SizedBox(height: Spacing.sm),
] else ...[
Row(
children: [
Text(fileState.fileIcon, style: const TextStyle(fontSize: 20)),
const Spacer(),
_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

View File

@@ -127,6 +127,43 @@ class TaskQueueNotifier extends Notifier<List<OutboundTask>> {
await _save();
}
Future<void> 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<void> cancelByConversation(String conversationId) async {
state = [
for (final t in state)

View File

@@ -107,6 +107,10 @@ class TaskWorker {
QueuedAttachmentStatus.failed => FileUploadStatus.failed,
QueuedAttachmentStatus.cancelled => FileUploadStatus.failed,
};
const imageExts = <String>{'.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)

View File

@@ -482,7 +482,7 @@ class TweakcnThemes {
preview: const <Color>[
Color(0xFFC96442),
Color(0xFFE9E6DC),
Color(0xFF9C87F5),
Color(0xFF1A1915),
],
);
@@ -508,7 +508,7 @@ class TweakcnThemes {
preview: const <Color>[
Color(0xFFA1A1AA),
Color(0xFFF4F4F5),
Color(0xFF2563EB),
Color(0xFF404040),
],
);

View File

@@ -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