File manage list

This commit is contained in:
2025-09-24 15:56:56 +08:00
parent d1a2f59dd1
commit 98eb28a4ec
8 changed files with 727 additions and 281 deletions

View File

@@ -1041,5 +1041,10 @@
"save": "Save", "save": "Save",
"webView": "Web View", "webView": "Web View",
"messageActions": "Message Actions", "messageActions": "Message Actions",
"viewEmbedLoadHint": "Tap to load" "viewEmbedLoadHint": "Tap to load",
"files": "Files",
"confirmDeleteFile": "Are you sure you want to delete this file?",
"deleteFile": "Delete File",
"failedToDeleteFile": "Failed to delete file",
"drive": "Drive"
} }

View File

@@ -148,7 +148,7 @@ class AppDatabase extends _$AppDatabase {
..where((m) => m.roomId.equals(roomId)); ..where((m) => m.roomId.equals(roomId));
if (query.isNotEmpty) { if (query.isNotEmpty) {
final searchTerm = '%${query}%'; final searchTerm = '%$query%';
selectStatement = selectStatement =
selectStatement..where( selectStatement..where(
(m) => (m) =>

View File

@@ -18,6 +18,7 @@ import 'package:island/screens/developers/edit_project.dart';
import 'package:island/screens/developers/new_project.dart'; import 'package:island/screens/developers/new_project.dart';
import 'package:island/screens/developers/project_detail.dart'; import 'package:island/screens/developers/project_detail.dart';
import 'package:island/screens/discovery/articles.dart'; import 'package:island/screens/discovery/articles.dart';
import 'package:island/screens/files/file_list.dart';
import 'package:island/screens/posts/post_categories_list.dart'; import 'package:island/screens/posts/post_categories_list.dart';
import 'package:island/screens/posts/post_category_detail.dart'; import 'package:island/screens/posts/post_category_detail.dart';
import 'package:island/screens/posts/post_search.dart'; import 'package:island/screens/posts/post_search.dart';
@@ -654,6 +655,11 @@ final routerProvider = Provider<GoRouter>((ref) {
path: '/account/wallet', path: '/account/wallet',
builder: (context, state) => const WalletScreen(), builder: (context, state) => const WalletScreen(),
), ),
GoRoute(
name: 'files',
path: '/account/files',
builder: (context, state) => const FileListScreen(),
),
GoRoute( GoRoute(
name: 'relationships', name: 'relationships',
path: '/account/relationships', path: '/account/relationships',

View File

@@ -304,6 +304,16 @@ class AccountScreen extends HookConsumerWidget {
context.pushNamed('wallet'); context.pushNamed('wallet');
}, },
), ),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.files),
trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('files').tr(),
onTap: () {
context.pushNamed('files');
},
),
ListTile( ListTile(
minTileHeight: 48, minTileHeight: 48,
leading: const Icon(Symbols.people), leading: const Icon(Symbols.people),

View File

@@ -0,0 +1,353 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart';
import 'package:island/pods/network.dart';
import 'package:island/utils/format.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/file_info_sheet.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:styled_widget/styled_widget.dart';
part 'file_list.g.dart';
@riverpod
class CloudFileListNotifier extends _$CloudFileListNotifier
with CursorPagingNotifierMixin<SnCloudFile> {
@override
Future<CursorPagingData<SnCloudFile>> build() => fetch(cursor: null);
@override
Future<CursorPagingData<SnCloudFile>> fetch({required String? cursor}) async {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final take = 20;
final queryParameters = {'offset': offset, 'take': take};
final response = await client.get(
'/drive/files/me',
queryParameters: queryParameters,
);
final List<SnCloudFile> items =
(response.data as List)
.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>))
.toList();
final total = int.parse(response.headers.value('X-Total') ?? '0');
final hasMore = offset + items.length < total;
final nextCursor = hasMore ? (offset + items.length).toString() : null;
return CursorPagingData(
items: items,
hasMore: hasMore,
nextCursor: nextCursor,
);
}
}
@riverpod
Future<Map<String, dynamic>?> billingUsage(Ref ref) async {
final client = ref.read(apiClientProvider);
final response = await client.get('/drive/billing/usage');
return response.data;
}
@riverpod
Future<Map<String, dynamic>?> billingQuota(Ref ref) async {
final client = ref.read(apiClientProvider);
final response = await client.get('/drive/billing/quota');
return response.data;
}
class FileListScreen extends HookConsumerWidget {
const FileListScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final usageAsync = ref.watch(billingUsageProvider);
final quotaAsync = ref.watch(billingQuotaProvider);
return AppScaffold(
appBar: AppBar(title: Text('Files')),
body: usageAsync.when(
data:
(usage) => quotaAsync.when(
data: (quota) => _buildQuotaUI(usage, quota, ref),
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('Error loading quota')),
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('Error loading usage')),
),
);
}
Widget _buildQuotaUI(
Map<String, dynamic>? usage,
Map<String, dynamic>? quota,
WidgetRef ref,
) {
if (usage == null) return const SizedBox.shrink();
return CustomScrollView(
slivers: [
const SliverGap(8),
SliverToBoxAdapter(
child: Column(
children: [
Row(
children: [
Expanded(
child: _buildStatCard(
'All Uploads',
'${((usage['total_usage_bytes'] as num) / (1024 * 1024 * 1024)).toStringAsFixed(3)} GiB',
),
),
Expanded(
child: _buildStatCard(
'All Files',
'${usage['total_file_count']}',
),
),
],
),
Row(
children: [
Expanded(
child: _buildStatCard(
'Quota',
'${usage['total_quota']} MiB',
),
),
Expanded(
child: _buildStatCard(
'Used Quota',
'${((usage['used_quota'] as num) / (usage['total_quota'] as num) * 100).toStringAsFixed(2)}%',
progress:
(usage['used_quota'] as num) /
(usage['total_quota'] as num),
),
),
],
),
],
).padding(horizontal: 8),
),
SliverToBoxAdapter(
child: Row(
children: [
Expanded(
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
const Text('Pool Usage'),
SizedBox(
height: 200,
child: PieChart(_buildPoolChartData(usage)),
),
],
),
),
),
),
Expanded(
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
const Text('Verbose Quota'),
SizedBox(
height: 200,
child: PieChart(_buildQuotaChartData(quota)),
),
],
),
),
),
),
],
).padding(horizontal: 8),
),
const SliverGap(8),
PagingHelperSliverView(
provider: cloudFileListNotifierProvider,
futureRefreshable: cloudFileListNotifierProvider.future,
notifierRefreshable: cloudFileListNotifierProvider.notifier,
contentBuilder:
(data, widgetCount, endItemView) => SliverList.builder(
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
return endItemView;
}
final item = data.items[index];
final itemType = item.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: item),
'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:
item.name.isEmpty
? Text('untitled').tr().italic()
: Text(
item.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(formatFileSize(item.size)),
onTap: () {
showModalBottomSheet(
useRootNavigator: true,
context: context,
isScrollControlled: true,
builder: (context) => FileInfoSheet(item: item),
);
},
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/${item.id}');
ref.invalidate(cloudFileListNotifierProvider);
} catch (e) {
showSnackBar('failedToDeleteFile'.tr());
} finally {
if (context.mounted) hideLoadingModal(context);
}
},
),
);
},
),
),
],
);
}
PieChartData _buildPoolChartData(Map<String, dynamic> usage) {
final pools = usage['pool_usages'] as List<dynamic>;
final colors = [
Colors.blue,
Colors.green,
Colors.orange,
Colors.red,
Colors.purple,
];
return PieChartData(
sections:
pools.asMap().entries.map((entry) {
final pool = entry.value as Map<String, dynamic>;
final title = pool['pool_name'] as String;
final truncatedTitle =
title.length > 8 ? '${title.substring(0, 8)}...' : title;
return PieChartSectionData(
value: (pool['usage_bytes'] as num).toDouble(),
title: truncatedTitle,
color: colors[entry.key % colors.length],
radius: 60,
titleStyle: const TextStyle(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.bold,
),
);
}).toList(),
);
}
PieChartData _buildQuotaChartData(Map<String, dynamic>? quota) {
if (quota == null) return PieChartData(sections: []);
return PieChartData(
sections: [
PieChartSectionData(
value: (quota['based_quota'] as num).toDouble(),
title: 'Base',
color: Colors.green,
radius: 60,
titleStyle: const TextStyle(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
PieChartSectionData(
value: (quota['extra_quota'] as num).toDouble(),
title: 'Extra',
color: Colors.orange,
radius: 60,
titleStyle: const TextStyle(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
);
}
Widget _buildStatCard(String label, String value, {double? progress}) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(label, style: const TextStyle(fontSize: 14)),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
value,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
if (progress != null) ...[
const SizedBox(height: 8),
SizedBox(
width: 28,
height: 28,
child: CircularProgressIndicator(value: progress),
),
],
],
),
],
),
),
);
}
}

View File

@@ -0,0 +1,69 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'file_list.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$billingUsageHash() => r'270ec8499378ee0c038aa44ad1c2e3ad9025740a';
/// See also [billingUsage].
@ProviderFor(billingUsage)
final billingUsageProvider =
AutoDisposeFutureProvider<Map<String, dynamic>?>.internal(
billingUsage,
name: r'billingUsageProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$billingUsageHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef BillingUsageRef = AutoDisposeFutureProviderRef<Map<String, dynamic>?>;
String _$billingQuotaHash() => r'0696b500fa8bb1270641bcacf262be58caff9b38';
/// See also [billingQuota].
@ProviderFor(billingQuota)
final billingQuotaProvider =
AutoDisposeFutureProvider<Map<String, dynamic>?>.internal(
billingQuota,
name: r'billingQuotaProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$billingQuotaHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef BillingQuotaRef = AutoDisposeFutureProviderRef<Map<String, dynamic>?>;
String _$cloudFileListNotifierHash() =>
r'e2c8a076a9e635c7b43a87d00f78775427ba6334';
/// See also [CloudFileListNotifier].
@ProviderFor(CloudFileListNotifier)
final cloudFileListNotifierProvider = AutoDisposeAsyncNotifierProvider<
CloudFileListNotifier,
CursorPagingData<SnCloudFile>
>.internal(
CloudFileListNotifier.new,
name: r'cloudFileListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$cloudFileListNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$CloudFileListNotifier =
AutoDisposeAsyncNotifier<CursorPagingData<SnCloudFile>>;
// 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

View File

@@ -1,15 +1,12 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:ui'; import 'dart:ui';
import 'package:dismissible_page/dismissible_page.dart'; import 'package:dismissible_page/dismissible_page.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_blurhash/flutter_blurhash.dart'; import 'package:flutter_blurhash/flutter_blurhash.dart';
import 'package:file_saver/file_saver.dart'; import 'package:file_saver/file_saver.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:gal/gal.dart'; import 'package:gal/gal.dart';
@@ -17,11 +14,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/utils/format.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/file_info_sheet.dart';
import 'package:island/widgets/content/sensitive.dart'; import 'package:island/widgets/content/sensitive.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:path/path.dart' show extension; import 'package:path/path.dart' show extension;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@@ -361,284 +357,11 @@ class CloudFileZoomIn extends HookConsumerWidget {
} }
void showInfoSheet() { void showInfoSheet() {
final theme = Theme.of(context);
final exifData = item.fileMeta?['exif'] as Map<String, dynamic>? ?? {};
showModalBottomSheet( showModalBottomSheet(
useRootNavigator: true, useRootNavigator: true,
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => FileInfoSheet(item: item),
(context) => SheetScaffold(
titleText: 'File Information',
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text('mimeType').tr(),
Text(
item.mimeType ?? 'unknown'.tr(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
),
SizedBox(height: 28, child: const VerticalDivider()),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text('fileSize').tr(),
Text(
formatFileSize(item.size),
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
),
if (item.hash != null)
SizedBox(height: 28, child: const VerticalDivider()),
if (item.hash != null)
Expanded(
child: GestureDetector(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text('fileHash').tr(),
Text(
'${item.hash!.substring(0, 6)}...',
style: theme.textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.bold),
),
],
),
onLongPress: () {
Clipboard.setData(
ClipboardData(text: item.hash!),
);
showSnackBar('File hash copied to clipboard');
},
),
),
],
).padding(horizontal: 24, vertical: 16),
const Divider(height: 1),
ListTile(
leading: const Icon(Symbols.tag),
title: Text('ID').tr(),
subtitle: Text(
item.id,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
trailing: IconButton(
icon: const Icon(Icons.copy),
onPressed: () {
Clipboard.setData(ClipboardData(text: item.id));
showSnackBar('File ID copied to clipboard');
},
),
),
ListTile(
leading: const Icon(Symbols.file_present),
title: Text('Name').tr(),
subtitle: Text(
item.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
trailing: IconButton(
icon: const Icon(Icons.copy),
onPressed: () {
Clipboard.setData(ClipboardData(text: item.name));
showSnackBar('File name copied to clipboard');
},
),
),
if (exifData.isNotEmpty) ...[
const Divider(height: 1),
Theme(
data: theme.copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
tilePadding: const EdgeInsets.symmetric(
horizontal: 24,
),
title: Text(
'exifData'.tr(),
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...exifData.entries.map(
(entry) => ListTile(
dense: true,
contentPadding: EdgeInsets.symmetric(
horizontal: 24,
),
title:
Text(
entry.key.contains('-')
? entry.key.split('-').last
: entry.key,
style: theme.textTheme.bodyMedium
?.copyWith(
fontWeight: FontWeight.w500,
),
).bold(),
subtitle: Text(
'${entry.value}'.isNotEmpty
? '${entry.value}'
: 'N/A',
style: theme.textTheme.bodyMedium,
),
onTap: () {
Clipboard.setData(
ClipboardData(text: '${entry.value}'),
);
showSnackBar('Value copied to clipboard');
},
),
),
],
),
],
),
),
],
if (item.fileMeta != null && item.fileMeta!.isNotEmpty) ...[
const Divider(height: 1),
Theme(
data: theme.copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
tilePadding: const EdgeInsets.symmetric(
horizontal: 24,
),
title: Text(
'File Metadata',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...item.fileMeta!.entries.map(
(entry) => ListTile(
dense: true,
contentPadding: EdgeInsets.symmetric(
horizontal: 24,
),
title:
Text(
entry.key,
style: theme.textTheme.bodyMedium
?.copyWith(
fontWeight: FontWeight.w500,
),
).bold(),
subtitle: Text(
jsonEncode(entry.value),
style: theme.textTheme.bodyMedium,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
onTap: () {
Clipboard.setData(
ClipboardData(
text: jsonEncode(entry.value),
),
);
showSnackBar('Value copied to clipboard');
},
),
),
],
),
],
),
),
],
if (item.userMeta != null && item.userMeta!.isNotEmpty) ...[
const Divider(height: 1),
Theme(
data: theme.copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
tilePadding: const EdgeInsets.symmetric(
horizontal: 24,
),
title: Text(
'User Metadata',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...item.userMeta!.entries.map(
(entry) => ListTile(
dense: true,
contentPadding: EdgeInsets.symmetric(
horizontal: 24,
),
title:
Text(
entry.key,
style: theme.textTheme.bodyMedium
?.copyWith(
fontWeight: FontWeight.w500,
),
).bold(),
subtitle: Text(
jsonEncode(entry.value),
style: theme.textTheme.bodyMedium,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
onTap: () {
Clipboard.setData(
ClipboardData(
text: jsonEncode(entry.value),
),
);
showSnackBar('Value copied to clipboard');
},
),
),
],
),
],
),
),
],
const SizedBox(height: 16),
],
),
),
),
); );
} }

View File

@@ -0,0 +1,280 @@
import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:island/models/file.dart';
import 'package:island/utils/format.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
class FileInfoSheet extends StatelessWidget {
final SnCloudFile item;
const FileInfoSheet({super.key, required this.item});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final exifData = item.fileMeta?['exif'] as Map<String, dynamic>? ?? {};
return SheetScaffold(
titleText: 'File Information',
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text('mimeType').tr(),
Text(
item.mimeType ?? 'unknown'.tr(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
),
SizedBox(height: 28, child: const VerticalDivider()),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text('fileSize').tr(),
Text(
formatFileSize(item.size),
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
),
if (item.hash != null)
SizedBox(height: 28, child: const VerticalDivider()),
if (item.hash != null)
Expanded(
child: GestureDetector(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text('fileHash').tr(),
Text(
'${item.hash!.substring(0, 6)}...',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
onLongPress: () {
Clipboard.setData(ClipboardData(text: item.hash!));
showSnackBar('File hash copied to clipboard');
},
),
),
],
).padding(horizontal: 24, vertical: 16),
const Divider(height: 1),
ListTile(
leading: const Icon(Symbols.tag),
title: Text('ID').tr(),
subtitle: Text(
item.id,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
trailing: IconButton(
icon: const Icon(Icons.copy),
onPressed: () {
Clipboard.setData(ClipboardData(text: item.id));
showSnackBar('File ID copied to clipboard');
},
),
),
ListTile(
leading: const Icon(Symbols.file_present),
title: Text('Name').tr(),
subtitle: Text(
item.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
trailing: IconButton(
icon: const Icon(Icons.copy),
onPressed: () {
Clipboard.setData(ClipboardData(text: item.name));
showSnackBar('File name copied to clipboard');
},
),
),
if (exifData.isNotEmpty) ...[
const Divider(height: 1),
Theme(
data: theme.copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
tilePadding: const EdgeInsets.symmetric(horizontal: 24),
title: Text(
'exifData'.tr(),
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...exifData.entries.map(
(entry) => ListTile(
dense: true,
contentPadding: EdgeInsets.symmetric(
horizontal: 24,
),
title:
Text(
entry.key.contains('-')
? entry.key.split('-').last
: entry.key,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
).bold(),
subtitle: Text(
'${entry.value}'.isNotEmpty
? '${entry.value}'
: 'N/A',
style: theme.textTheme.bodyMedium,
),
onTap: () {
Clipboard.setData(
ClipboardData(text: '${entry.value}'),
);
showSnackBar('Value copied to clipboard');
},
),
),
],
),
],
),
),
],
if (item.fileMeta != null && item.fileMeta!.isNotEmpty) ...[
const Divider(height: 1),
Theme(
data: theme.copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
tilePadding: const EdgeInsets.symmetric(horizontal: 24),
title: Text(
'File Metadata',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...item.fileMeta!.entries.map(
(entry) => ListTile(
dense: true,
contentPadding: EdgeInsets.symmetric(
horizontal: 24,
),
title:
Text(
entry.key,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
).bold(),
subtitle: Text(
jsonEncode(entry.value),
style: theme.textTheme.bodyMedium,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
onTap: () {
Clipboard.setData(
ClipboardData(text: jsonEncode(entry.value)),
);
showSnackBar('Value copied to clipboard');
},
),
),
],
),
],
),
),
],
if (item.userMeta != null && item.userMeta!.isNotEmpty) ...[
const Divider(height: 1),
Theme(
data: theme.copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
tilePadding: const EdgeInsets.symmetric(horizontal: 24),
title: Text(
'User Metadata',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...item.userMeta!.entries.map(
(entry) => ListTile(
dense: true,
contentPadding: EdgeInsets.symmetric(
horizontal: 24,
),
title:
Text(
entry.key,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
).bold(),
subtitle: Text(
jsonEncode(entry.value),
style: theme.textTheme.bodyMedium,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
onTap: () {
Clipboard.setData(
ClipboardData(text: jsonEncode(entry.value)),
);
showSnackBar('Value copied to clipboard');
},
),
),
],
),
],
),
),
],
const SizedBox(height: 16),
],
),
),
);
}
}