Unindexed files

This commit is contained in:
2025-11-15 02:59:20 +08:00
parent 0d11435feb
commit 74fa2215a6
7 changed files with 540 additions and 161 deletions

View File

@@ -8,4 +8,6 @@ part 'file_list_item.freezed.dart';
sealed class FileListItem with _$FileListItem { sealed class FileListItem with _$FileListItem {
const factory FileListItem.file(SnCloudFileIndex fileIndex) = FileItem; const factory FileListItem.file(SnCloudFileIndex fileIndex) = FileItem;
const factory FileListItem.folder(SnCloudFolder folder) = FolderItem; const factory FileListItem.folder(SnCloudFolder folder) = FolderItem;
const factory FileListItem.unindexedFile(SnCloudFile file) =
UnindexedFileItem;
} }

View File

@@ -55,12 +55,13 @@ extension FileListItemPatterns on FileListItem {
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( FileItem value)? file,TResult Function( FolderItem value)? folder,required TResult orElse(),}){ @optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( FileItem value)? file,TResult Function( FolderItem value)? folder,TResult Function( UnindexedFileItem value)? unindexedFile,required TResult orElse(),}){
final _that = this; final _that = this;
switch (_that) { switch (_that) {
case FileItem() when file != null: case FileItem() when file != null:
return file(_that);case FolderItem() when folder != null: return file(_that);case FolderItem() when folder != null:
return folder(_that);case _: return folder(_that);case UnindexedFileItem() when unindexedFile != null:
return unindexedFile(_that);case _:
return orElse(); return orElse();
} }
@@ -78,12 +79,13 @@ return folder(_that);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( FileItem value) file,required TResult Function( FolderItem value) folder,}){ @optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( FileItem value) file,required TResult Function( FolderItem value) folder,required TResult Function( UnindexedFileItem value) unindexedFile,}){
final _that = this; final _that = this;
switch (_that) { switch (_that) {
case FileItem(): case FileItem():
return file(_that);case FolderItem(): return file(_that);case FolderItem():
return folder(_that);} return folder(_that);case UnindexedFileItem():
return unindexedFile(_that);}
} }
/// A variant of `map` that fallback to returning `null`. /// A variant of `map` that fallback to returning `null`.
/// ///
@@ -97,12 +99,13 @@ return folder(_that);}
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( FileItem value)? file,TResult? Function( FolderItem value)? folder,}){ @optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( FileItem value)? file,TResult? Function( FolderItem value)? folder,TResult? Function( UnindexedFileItem value)? unindexedFile,}){
final _that = this; final _that = this;
switch (_that) { switch (_that) {
case FileItem() when file != null: case FileItem() when file != null:
return file(_that);case FolderItem() when folder != null: return file(_that);case FolderItem() when folder != null:
return folder(_that);case _: return folder(_that);case UnindexedFileItem() when unindexedFile != null:
return unindexedFile(_that);case _:
return null; return null;
} }
@@ -119,11 +122,12 @@ return folder(_that);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( SnCloudFileIndex fileIndex)? file,TResult Function( SnCloudFolder folder)? folder,required TResult orElse(),}) {final _that = this; @optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( SnCloudFileIndex fileIndex)? file,TResult Function( SnCloudFolder folder)? folder,TResult Function( SnCloudFile file)? unindexedFile,required TResult orElse(),}) {final _that = this;
switch (_that) { switch (_that) {
case FileItem() when file != null: case FileItem() when file != null:
return file(_that.fileIndex);case FolderItem() when folder != null: return file(_that.fileIndex);case FolderItem() when folder != null:
return folder(_that.folder);case _: return folder(_that.folder);case UnindexedFileItem() when unindexedFile != null:
return unindexedFile(_that.file);case _:
return orElse(); return orElse();
} }
@@ -141,11 +145,12 @@ return folder(_that.folder);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( SnCloudFileIndex fileIndex) file,required TResult Function( SnCloudFolder folder) folder,}) {final _that = this; @optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( SnCloudFileIndex fileIndex) file,required TResult Function( SnCloudFolder folder) folder,required TResult Function( SnCloudFile file) unindexedFile,}) {final _that = this;
switch (_that) { switch (_that) {
case FileItem(): case FileItem():
return file(_that.fileIndex);case FolderItem(): return file(_that.fileIndex);case FolderItem():
return folder(_that.folder);} return folder(_that.folder);case UnindexedFileItem():
return unindexedFile(_that.file);}
} }
/// A variant of `when` that fallback to returning `null` /// A variant of `when` that fallback to returning `null`
/// ///
@@ -159,11 +164,12 @@ return folder(_that.folder);}
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( SnCloudFileIndex fileIndex)? file,TResult? Function( SnCloudFolder folder)? folder,}) {final _that = this; @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( SnCloudFileIndex fileIndex)? file,TResult? Function( SnCloudFolder folder)? folder,TResult? Function( SnCloudFile file)? unindexedFile,}) {final _that = this;
switch (_that) { switch (_that) {
case FileItem() when file != null: case FileItem() when file != null:
return file(_that.fileIndex);case FolderItem() when folder != null: return file(_that.fileIndex);case FolderItem() when folder != null:
return folder(_that.folder);case _: return folder(_that.folder);case UnindexedFileItem() when unindexedFile != null:
return unindexedFile(_that.file);case _:
return null; return null;
} }
@@ -321,4 +327,79 @@ $SnCloudFolderCopyWith<$Res> get folder {
} }
} }
/// @nodoc
class UnindexedFileItem implements FileListItem {
const UnindexedFileItem(this.file);
final SnCloudFile file;
/// Create a copy of FileListItem
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$UnindexedFileItemCopyWith<UnindexedFileItem> get copyWith => _$UnindexedFileItemCopyWithImpl<UnindexedFileItem>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is UnindexedFileItem&&(identical(other.file, file) || other.file == file));
}
@override
int get hashCode => Object.hash(runtimeType,file);
@override
String toString() {
return 'FileListItem.unindexedFile(file: $file)';
}
}
/// @nodoc
abstract mixin class $UnindexedFileItemCopyWith<$Res> implements $FileListItemCopyWith<$Res> {
factory $UnindexedFileItemCopyWith(UnindexedFileItem value, $Res Function(UnindexedFileItem) _then) = _$UnindexedFileItemCopyWithImpl;
@useResult
$Res call({
SnCloudFile file
});
$SnCloudFileCopyWith<$Res> get file;
}
/// @nodoc
class _$UnindexedFileItemCopyWithImpl<$Res>
implements $UnindexedFileItemCopyWith<$Res> {
_$UnindexedFileItemCopyWithImpl(this._self, this._then);
final UnindexedFileItem _self;
final $Res Function(UnindexedFileItem) _then;
/// Create a copy of FileListItem
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') $Res call({Object? file = null,}) {
return _then(UnindexedFileItem(
null == file ? _self.file : file // ignore: cast_nullable_to_non_nullable
as SnCloudFile,
));
}
/// Create a copy of FileListItem
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res> get file {
return $SnCloudFileCopyWith<$Res>(_self.file, (value) {
return _then(_self.copyWith(file: value));
});
}
}
// dart format on // dart format on

View File

@@ -58,6 +58,47 @@ Future<Map<String, dynamic>?> billingUsage(Ref ref) async {
return response.data; return response.data;
} }
@riverpod
class UnindexedFileListNotifier extends _$UnindexedFileListNotifier
with CursorPagingNotifierMixin<FileListItem> {
@override
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
@override
Future<CursorPagingData<FileListItem>> fetch({
required String? cursor,
}) async {
final client = ref.read(apiClientProvider);
final offset = cursor != null ? int.tryParse(cursor) ?? 0 : 0;
const take = 50; // Default page size
final response = await client.get(
'/drive/index/unindexed',
queryParameters: {'take': take.toString(), 'offset': offset.toString()},
);
final total = int.tryParse(response.headers.value('x-total') ?? '0') ?? 0;
final List<SnCloudFile> files =
(response.data as List)
.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>))
.toList();
final List<FileListItem> items =
files.map((file) => FileListItem.unindexedFile(file)).toList();
final hasMore = offset + take < total;
final nextCursor = hasMore ? (offset + take).toString() : null;
return CursorPagingData(
items: items,
hasMore: hasMore,
nextCursor: nextCursor,
);
}
}
@riverpod @riverpod
Future<Map<String, dynamic>?> billingQuota(Ref ref) async { Future<Map<String, dynamic>?> billingQuota(Ref ref) async {
final client = ref.read(apiClientProvider); final client = ref.read(apiClientProvider);

View File

@@ -45,7 +45,7 @@ final billingQuotaProvider =
// ignore: unused_element // ignore: unused_element
typedef BillingQuotaRef = AutoDisposeFutureProviderRef<Map<String, dynamic>?>; typedef BillingQuotaRef = AutoDisposeFutureProviderRef<Map<String, dynamic>?>;
String _$cloudFileListNotifierHash() => String _$cloudFileListNotifierHash() =>
r'44c17a8ef959bbef5d07132603a722f76d39b9e9'; r'5b919f2212ce64c567b9f31912ed18fc4e4bc87d';
/// See also [CloudFileListNotifier]. /// See also [CloudFileListNotifier].
@ProviderFor(CloudFileListNotifier) @ProviderFor(CloudFileListNotifier)
@@ -65,5 +65,26 @@ final cloudFileListNotifierProvider = AutoDisposeAsyncNotifierProvider<
typedef _$CloudFileListNotifier = typedef _$CloudFileListNotifier =
AutoDisposeAsyncNotifier<CursorPagingData<FileListItem>>; AutoDisposeAsyncNotifier<CursorPagingData<FileListItem>>;
String _$unindexedFileListNotifierHash() =>
r'48fc92432a50a562190da5fe8ed0920d171b07b6';
/// See also [UnindexedFileListNotifier].
@ProviderFor(UnindexedFileListNotifier)
final unindexedFileListNotifierProvider = AutoDisposeAsyncNotifierProvider<
UnindexedFileListNotifier,
CursorPagingData<FileListItem>
>.internal(
UnindexedFileListNotifier.new,
name: r'unindexedFileListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$unindexedFileListNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$UnindexedFileListNotifier =
AutoDisposeAsyncNotifier<CursorPagingData<FileListItem>>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -21,6 +21,7 @@ class FileListScreen extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
// Path navigation state // Path navigation state
final currentPath = useState<String>('/'); final currentPath = useState<String>('/');
final mode = useState<FileListMode>(FileListMode.normal);
final usageAsync = ref.watch(billingUsageProvider); final usageAsync = ref.watch(billingUsageProvider);
final quotaAsync = ref.watch(billingQuotaProvider); final quotaAsync = ref.watch(billingQuotaProvider);
@@ -54,6 +55,7 @@ class FileListScreen extends HookConsumerWidget {
onPickAndUpload: onPickAndUpload:
() => _pickAndUploadFile(ref, currentPath.value), () => _pickAndUploadFile(ref, currentPath.value),
onShowCreateDirectory: _showCreateDirectoryDialog, onShowCreateDirectory: _showCreateDirectoryDialog,
mode: mode,
), ),
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('Error loading quota')), error: (e, _) => Center(child: Text('Error loading quota')),

View File

@@ -14,12 +14,15 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
enum FileListMode { normal, unindexed }
class FileListView extends HookConsumerWidget { class FileListView extends HookConsumerWidget {
final Map<String, dynamic>? usage; final Map<String, dynamic>? usage;
final Map<String, dynamic>? quota; final Map<String, dynamic>? quota;
final ValueNotifier<String> currentPath; final ValueNotifier<String> currentPath;
final VoidCallback onPickAndUpload; final VoidCallback onPickAndUpload;
final Function(BuildContext, ValueNotifier<String>) onShowCreateDirectory; final Function(BuildContext, ValueNotifier<String>) onShowCreateDirectory;
final ValueNotifier<FileListMode> mode;
const FileListView({ const FileListView({
required this.usage, required this.usage,
@@ -27,24 +30,130 @@ class FileListView extends HookConsumerWidget {
required this.currentPath, required this.currentPath,
required this.onPickAndUpload, required this.onPickAndUpload,
required this.onShowCreateDirectory, required this.onShowCreateDirectory,
required this.mode,
super.key, super.key,
}); });
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
useEffect(() { useEffect(() {
if (mode.value == FileListMode.normal) {
final notifier = ref.read(cloudFileListNotifierProvider.notifier); final notifier = ref.read(cloudFileListNotifierProvider.notifier);
notifier.setPath(currentPath.value); notifier.setPath(currentPath.value);
}
return null; return null;
}, [currentPath.value]); }, [currentPath.value, mode.value]);
if (usage == null) return const SizedBox.shrink(); if (usage == null) return const SizedBox.shrink();
return CustomScrollView(
slivers: [ final bodyWidget = switch (mode.value) {
const SliverGap(8), FileListMode.unindexed => PagingHelperSliverView(
SliverToBoxAdapter(child: _buildPathNavigation(ref, currentPath)), provider: unindexedFileListNotifierProvider,
const SliverGap(8), futureRefreshable: unindexedFileListNotifierProvider.future,
PagingHelperSliverView( notifierRefreshable: unindexedFileListNotifierProvider.notifier,
contentBuilder:
(data, widgetCount, endItemView) =>
data.items.isEmpty
? SliverToBoxAdapter(
child: _buildEmptyUnindexedFilesHint(ref),
)
: SliverList.builder(
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
return endItemView;
}
final item = data.items[index];
return item.map(
file: (fileItem) {
// This should not happen in unindexed mode
return const SizedBox.shrink();
},
folder: (folderItem) {
// This should not happen in unindexed mode
return const SizedBox.shrink();
},
unindexedFile: (unindexedFileItem) {
final file = unindexedFileItem.file;
final itemType =
file.mimeType?.split('/').firstOrNull;
return ListTile(
leading: ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
child: SizedBox(
height: 48,
width: 48,
child: switch (itemType) {
'image' => CloudImageWidget(file: file),
'audio' =>
const Icon(
Symbols.audio_file,
fill: 1,
).center(),
'video' =>
const Icon(
Symbols.video_file,
fill: 1,
).center(),
_ =>
const Icon(
Symbols.body_system,
fill: 1,
).center(),
},
),
),
title:
file.name.isEmpty
? Text('untitled').tr().italic()
: Text(
file.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(formatFileSize(file.size)),
onTap: () {
context.push('/files/${file.id}', extra: file);
},
trailing: IconButton(
icon: const Icon(Symbols.delete),
onPressed: () async {
final confirmed = await showConfirmAlert(
'confirmDeleteFile'.tr(),
'deleteFile'.tr(),
);
if (!confirmed) return;
if (context.mounted) {
showLoadingModal(context);
}
try {
final client = ref.read(apiClientProvider);
await client.delete(
'/drive/files/${file.id}',
);
ref.invalidate(
unindexedFileListNotifierProvider,
);
} catch (e) {
showSnackBar('failedToDeleteFile'.tr());
} finally {
if (context.mounted) {
hideLoadingModal(context);
}
}
},
),
);
},
);
},
),
),
_ => PagingHelperSliverView(
provider: cloudFileListNotifierProvider, provider: cloudFileListNotifierProvider,
futureRefreshable: cloudFileListNotifierProvider.future, futureRefreshable: cloudFileListNotifierProvider.future,
notifierRefreshable: cloudFileListNotifierProvider.notifier, notifierRefreshable: cloudFileListNotifierProvider.notifier,
@@ -123,9 +232,7 @@ class FileListView extends HookConsumerWidget {
showLoadingModal(context); showLoadingModal(context);
} }
try { try {
final client = ref.read( final client = ref.read(apiClientProvider);
apiClientProvider,
);
await client.delete( await client.delete(
'/drive/index/remove/${fileItem.fileIndex.id}', '/drive/index/remove/${fileItem.fileIndex.id}',
); );
@@ -174,10 +281,30 @@ class FileListView extends HookConsumerWidget {
currentPath.value = newPath; currentPath.value = newPath;
}, },
), ),
unindexedFile: (unindexedFileItem) {
// This should not happen in normal mode
return const SizedBox.shrink();
},
); );
}, },
), ),
), ),
};
return Column(
children: [
const Gap(8),
_buildPathNavigation(ref, currentPath),
const Gap(8),
Expanded(
child: CustomScrollView(
slivers: [
bodyWidget,
if (mode.value == FileListMode.normal && currentPath.value == '/')
SliverToBoxAdapter(child: _buildUnindexedFilesEntry(ref)),
],
),
),
], ],
); );
} }
@@ -187,7 +314,16 @@ class FileListView extends HookConsumerWidget {
ValueNotifier<String> currentPath, ValueNotifier<String> currentPath,
) { ) {
Widget pathContent; Widget pathContent;
if (currentPath.value == '/') { if (mode.value == FileListMode.unindexed) {
pathContent = Row(
children: [
Text(
'Unindexed Files',
style: TextStyle(fontWeight: FontWeight.bold),
),
],
);
} else if (currentPath.value == '/') {
pathContent = Text( pathContent = Text(
'Root Directory', 'Root Directory',
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
@@ -234,28 +370,87 @@ class FileListView extends HookConsumerWidget {
); );
} }
return Card( return SizedBox(
height: 64,
child: Card(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Row( child: Row(
children: [ children: [
const Icon(Symbols.folder), IconButton(
icon: Icon(
mode.value == FileListMode.unindexed
? Symbols.inventory_2
: Symbols.folder,
),
onPressed: () {
if (mode.value == FileListMode.unindexed) {
mode.value = FileListMode.normal;
}
currentPath.value = '/';
},
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
),
),
const Gap(8), const Gap(8),
Expanded(child: pathContent), Expanded(child: pathContent),
if (mode.value == FileListMode.normal) ...[
IconButton( IconButton(
icon: const Icon(Symbols.create_new_folder), icon: const Icon(Symbols.create_new_folder),
onPressed: () => onShowCreateDirectory(ref.context, currentPath), onPressed:
() => onShowCreateDirectory(ref.context, currentPath),
tooltip: 'Create Directory', tooltip: 'Create Directory',
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
),
), ),
IconButton( IconButton(
icon: const Icon(Symbols.upload_file), icon: const Icon(Symbols.upload_file),
onPressed: onPickAndUpload, onPressed: onPickAndUpload,
tooltip: 'Upload File', tooltip: 'Upload File',
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
), ),
),
],
], ],
), ),
), ),
).padding(horizontal: 8); ).padding(horizontal: 8),
);
}
Widget _buildUnindexedFilesEntry(WidgetRef ref) {
return Container(
decoration: BoxDecoration(
border: Border.all(color: Theme.of(ref.context).colorScheme.outline),
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
margin: const EdgeInsets.symmetric(horizontal: 12),
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
const Icon(Symbols.inventory_2).padding(horizontal: 8),
const Gap(8),
const Text('Unindexed Files').bold(),
const Spacer(),
const Icon(Symbols.chevron_right).padding(horizontal: 8),
],
),
),
onTap: () {
mode.value = FileListMode.unindexed;
currentPath.value = '/';
},
),
);
} }
Widget _buildEmptyDirectoryHint( Widget _buildEmptyDirectoryHint(
@@ -263,7 +458,7 @@ class FileListView extends HookConsumerWidget {
ValueNotifier<String> currentPath, ValueNotifier<String> currentPath,
) { ) {
return Card( return Card(
margin: const EdgeInsets.fromLTRB(16, 0, 16, 16), margin: const EdgeInsets.fromLTRB(12, 0, 12, 16),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48),
child: Column( child: Column(
@@ -313,4 +508,39 @@ class FileListView extends HookConsumerWidget {
), ),
); );
} }
Widget _buildEmptyUnindexedFilesHint(WidgetRef ref) {
return Card(
margin: const EdgeInsets.fromLTRB(16, 0, 16, 0),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Symbols.inventory_2, size: 64, color: Colors.grey),
const Gap(16),
Text(
'No unindexed files',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Theme.of(ref.context).textTheme.bodyLarge?.color,
),
),
const Gap(8),
Text(
'All files have been assigned to paths.\n'
'Files without paths will appear here.',
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(
ref.context,
).textTheme.bodyMedium?.color?.withOpacity(0.7),
),
),
],
),
),
);
}
} }

View File

@@ -442,7 +442,9 @@ class _UploadTaskTileState extends State<UploadTaskTile>
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
widget.task.fileName, widget.task.fileName.isEmpty
? 'untitled'.tr()
: widget.task.fileName,
style: Theme.of( style: Theme.of(
context, context,
).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500), ).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500),