mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-25 03:42:13 +00:00
New: Manage storage view
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
This commit is contained in:
parent
5556532879
commit
e9b550023f
11 changed files with 355 additions and 145 deletions
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
## 0.2.13
|
||||
|
||||
- Improved: Media thumbnails for faster loading
|
||||
- New: Manage storage view.
|
||||
- Improved: Media thumbnails for faster loading.
|
||||
|
||||
## 0.2.12
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ class Routes {
|
|||
'/settings/privacy/user_discovery';
|
||||
static const String settingsNotification = '/settings/notification';
|
||||
static const String settingsStorage = '/settings/storage_data';
|
||||
static const String settingsStorageManage = '/settings/storage_data/manage';
|
||||
static const String settingsStorageImport = '/settings/storage_data/import';
|
||||
static const String settingsStorageExport = '/settings/storage_data/export';
|
||||
static const String settingsHelp = '/settings/help';
|
||||
|
|
|
|||
|
|
@ -184,4 +184,17 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
|||
final rows = await query.get();
|
||||
return rows.map((row) => row.readTable(db.messages).messageId).toList();
|
||||
}
|
||||
|
||||
Future<Map<MediaType, int>> getStorageStats() async {
|
||||
final rows = await select(mediaFiles).get();
|
||||
final stats = <MediaType, int>{};
|
||||
|
||||
for (final row in rows) {
|
||||
final type = row.type;
|
||||
final size = row.sizeInBytes ?? 0;
|
||||
stats[type] = (stats[type] ?? 0) + size;
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -542,6 +542,36 @@ abstract class AppLocalizations {
|
|||
/// **'When using WI-FI'**
|
||||
String get settingsStorageDataAutoDownWifi;
|
||||
|
||||
/// No description provided for @settingsStorageManageTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Manage storage'**
|
||||
String get settingsStorageManageTitle;
|
||||
|
||||
/// No description provided for @settingsStorageUsed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Storage used'**
|
||||
String get settingsStorageUsed;
|
||||
|
||||
/// No description provided for @settingsStorageImages.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Images'**
|
||||
String get settingsStorageImages;
|
||||
|
||||
/// No description provided for @settingsStorageVideos.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Videos'**
|
||||
String get settingsStorageVideos;
|
||||
|
||||
/// No description provided for @settingsStorageGifs.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'GIFs'**
|
||||
String get settingsStorageGifs;
|
||||
|
||||
/// No description provided for @settingsProfileCustomizeAvatar.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -249,6 +249,21 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get settingsStorageDataAutoDownWifi => 'Bei Nutzung von WLAN';
|
||||
|
||||
@override
|
||||
String get settingsStorageManageTitle => 'Speicher verwalten';
|
||||
|
||||
@override
|
||||
String get settingsStorageUsed => 'Speicherplatz belegt';
|
||||
|
||||
@override
|
||||
String get settingsStorageImages => 'Bilder';
|
||||
|
||||
@override
|
||||
String get settingsStorageVideos => 'Videos';
|
||||
|
||||
@override
|
||||
String get settingsStorageGifs => 'GIFs';
|
||||
|
||||
@override
|
||||
String get settingsProfileCustomizeAvatar => 'Avatar anpassen';
|
||||
|
||||
|
|
|
|||
|
|
@ -245,6 +245,21 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get settingsStorageDataAutoDownWifi => 'When using WI-FI';
|
||||
|
||||
@override
|
||||
String get settingsStorageManageTitle => 'Manage storage';
|
||||
|
||||
@override
|
||||
String get settingsStorageUsed => 'Storage used';
|
||||
|
||||
@override
|
||||
String get settingsStorageImages => 'Images';
|
||||
|
||||
@override
|
||||
String get settingsStorageVideos => 'Videos';
|
||||
|
||||
@override
|
||||
String get settingsStorageGifs => 'GIFs';
|
||||
|
||||
@override
|
||||
String get settingsProfileCustomizeAvatar => 'Customize your avatar';
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit f649128fd875a12f23518ff2641190cc129a9339
|
||||
Subproject commit 63f73aec47d84009ca50d2e8ad324b11eff59e9d
|
||||
|
|
@ -23,6 +23,7 @@ import 'package:twonly/src/visual/views/settings/chat/chat_settings.view.dart';
|
|||
import 'package:twonly/src/visual/views/settings/data_and_storage.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/data_and_storage/export_media.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/data_and_storage/import_media.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/data_and_storage/manage_storage.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/developer/automated_testing.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/developer/developer.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/developer/reduce_flames.view.dart';
|
||||
|
|
@ -210,6 +211,10 @@ final routerProvider = GoRouter(
|
|||
path: 'storage_data',
|
||||
builder: (context, state) => const DataAndStorageView(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'manage',
|
||||
builder: (context, state) => const ManageStorageView(),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'import',
|
||||
builder: (context, state) => const ImportMediaView(),
|
||||
|
|
|
|||
|
|
@ -89,14 +89,10 @@ class MemoriesService {
|
|||
final mediaFiles = await twonlyDB.mediaFilesDao.getMediaFilesByIds(
|
||||
mediaIds,
|
||||
);
|
||||
final mediaFileMap = {for (final m in mediaFiles) m.mediaId: m};
|
||||
|
||||
final allContacts = await twonlyDB.contactsDao.getAllContacts();
|
||||
final contactMap = {for (final c in allContacts) c.userId: c};
|
||||
|
||||
final now = clock.now();
|
||||
final tempGalleryItems = <MemoryItem>[];
|
||||
final tempGalleryItemsLastYears = <int, List<MemoryItem>>{};
|
||||
final mediaIdToSender = <String, Contact?>{};
|
||||
|
||||
for (final itemJson in itemList) {
|
||||
final map = itemJson as Map<String, dynamic>;
|
||||
|
|
@ -104,64 +100,14 @@ class MemoriesService {
|
|||
final senderUserId = map['senderUserId'] as int?;
|
||||
if (mediaId == null) continue;
|
||||
|
||||
final mediaFile = mediaFileMap[mediaId];
|
||||
if (mediaFile == null) continue;
|
||||
|
||||
final mediaService = MediaFileService(mediaFile);
|
||||
if (!mediaService.imagePreviewAvailable) continue;
|
||||
|
||||
final contact = senderUserId != null
|
||||
mediaIdToSender[mediaId] = senderUserId != null
|
||||
? contactMap[senderUserId]
|
||||
: null;
|
||||
final item = MemoryItem(
|
||||
mediaService: mediaService,
|
||||
messages: [],
|
||||
sender: contact,
|
||||
);
|
||||
tempGalleryItems.add(item);
|
||||
|
||||
if (mediaFile.createdAt.month == now.month &&
|
||||
mediaFile.createdAt.day == now.day) {
|
||||
final diff = now.year - mediaFile.createdAt.year;
|
||||
if (diff > 0) {
|
||||
tempGalleryItemsLastYears.putIfAbsent(diff, () => []).add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final tempOrderedByMonth = <String, List<int>>{};
|
||||
final tempMonths = <String>[];
|
||||
var lastMonth = '';
|
||||
|
||||
for (var i = 0; i < tempGalleryItems.length; i++) {
|
||||
final mFile = tempGalleryItems[i].mediaService.mediaFile;
|
||||
final month =
|
||||
mFile.createdAtMonth ??
|
||||
DateFormat('MMMM yyyy').format(mFile.createdAt);
|
||||
if (lastMonth != month) {
|
||||
lastMonth = month;
|
||||
tempMonths.add(month);
|
||||
}
|
||||
tempOrderedByMonth.putIfAbsent(month, () => []).add(i);
|
||||
}
|
||||
|
||||
for (final list in tempGalleryItemsLastYears.values) {
|
||||
list.sort(
|
||||
(a, b) => b.mediaService.mediaFile.createdAt.compareTo(
|
||||
a.mediaService.mediaFile.createdAt,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final sortedGalleryItemsLastYears =
|
||||
SplayTreeMap<int, List<MemoryItem>>.from(tempGalleryItemsLastYears);
|
||||
|
||||
_cachedState = MemoriesState(
|
||||
filesToMigrate: 0,
|
||||
galleryItems: tempGalleryItems,
|
||||
months: tempMonths,
|
||||
orderedByMonth: tempOrderedByMonth,
|
||||
galleryItemsLastYears: sortedGalleryItemsLastYears,
|
||||
_cachedState = _computeState(
|
||||
mediaFiles: mediaFiles,
|
||||
mediaIdToSender: mediaIdToSender,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
@ -169,14 +115,87 @@ class MemoriesService {
|
|||
}
|
||||
}
|
||||
|
||||
static MemoriesState _computeState({
|
||||
required List<MediaFile> mediaFiles,
|
||||
required Map<String, Contact?> mediaIdToSender,
|
||||
int filesToMigrate = 0,
|
||||
}) {
|
||||
final now = clock.now();
|
||||
final tempGalleryItems = <MemoryItem>[];
|
||||
final tempGalleryItemsLastYears = <int, List<MemoryItem>>{};
|
||||
|
||||
for (final mediaFile in mediaFiles) {
|
||||
final mediaService = MediaFileService(mediaFile);
|
||||
if (!mediaService.imagePreviewAvailable) continue;
|
||||
|
||||
final senderContact = mediaIdToSender[mediaFile.mediaId];
|
||||
final item = MemoryItem(
|
||||
mediaService: mediaService,
|
||||
messages: [],
|
||||
sender: senderContact,
|
||||
);
|
||||
|
||||
tempGalleryItems.add(item);
|
||||
|
||||
if (mediaFile.createdAt.month == now.month &&
|
||||
mediaFile.createdAt.day == now.day) {
|
||||
final diff = now.year - mediaFile.createdAt.year;
|
||||
if (diff > 0) {
|
||||
tempGalleryItemsLastYears.putIfAbsent(diff, () => []).add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort descending by creation date
|
||||
tempGalleryItems.sort(
|
||||
(a, b) => b.mediaService.mediaFile.createdAt.compareTo(
|
||||
a.mediaService.mediaFile.createdAt,
|
||||
),
|
||||
);
|
||||
|
||||
final tempOrderedByMonth = <String, List<int>>{};
|
||||
final tempMonths = <String>[];
|
||||
var lastMonth = '';
|
||||
|
||||
for (var i = 0; i < tempGalleryItems.length; i++) {
|
||||
final mFile = tempGalleryItems[i].mediaService.mediaFile;
|
||||
final month =
|
||||
mFile.createdAtMonth ??
|
||||
DateFormat('MMMM yyyy').format(mFile.createdAt);
|
||||
if (lastMonth != month) {
|
||||
lastMonth = month;
|
||||
tempMonths.add(month);
|
||||
}
|
||||
tempOrderedByMonth.putIfAbsent(month, () => []).add(i);
|
||||
}
|
||||
|
||||
for (final list in tempGalleryItemsLastYears.values) {
|
||||
list.sort(
|
||||
(a, b) => b.mediaService.mediaFile.createdAt.compareTo(
|
||||
a.mediaService.mediaFile.createdAt,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final sortedGalleryItemsLastYears =
|
||||
SplayTreeMap<int, List<MemoryItem>>.from(tempGalleryItemsLastYears);
|
||||
|
||||
return MemoriesState(
|
||||
filesToMigrate: filesToMigrate,
|
||||
galleryItems: tempGalleryItems,
|
||||
months: tempMonths,
|
||||
orderedByMonth: tempOrderedByMonth,
|
||||
galleryItemsLastYears: sortedGalleryItemsLastYears,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _initAsync() async {
|
||||
try {
|
||||
// 1. Perform Inventory / Migration of stored files
|
||||
final pendingFiles = await twonlyDB.mediaFilesDao
|
||||
.getAllMediaFilesPendingMigration();
|
||||
|
||||
if (pendingFiles.isNotEmpty) {
|
||||
_updateState(filesToMigrate: pendingFiles.length);
|
||||
_updateMigrationCount(pendingFiles.length);
|
||||
|
||||
for (final mediaFile in pendingFiles) {
|
||||
final mediaService = MediaFileService(mediaFile);
|
||||
|
|
@ -203,13 +222,12 @@ class MemoriesService {
|
|||
await mediaService.createThumbnail();
|
||||
}
|
||||
}
|
||||
_updateState(filesToMigrate: _currentState.filesToMigrate - 1);
|
||||
_updateMigrationCount(_currentState.filesToMigrate - 1);
|
||||
}
|
||||
|
||||
_updateState(filesToMigrate: 0);
|
||||
_updateMigrationCount(0);
|
||||
}
|
||||
|
||||
// 2. Subscribe to stored media files stream
|
||||
await _dbSubscription?.cancel();
|
||||
_dbSubscription = twonlyDB.mediaFilesDao
|
||||
.watchAllStoredMediaFiles()
|
||||
|
|
@ -221,11 +239,6 @@ class MemoriesService {
|
|||
|
||||
Future<void> _processMediaFilesStream(List<MediaFile> mediaFiles) async {
|
||||
try {
|
||||
final now = clock.now();
|
||||
final tempGalleryItems = <MemoryItem>[];
|
||||
final tempGalleryItemsLastYears = <int, List<MemoryItem>>{};
|
||||
|
||||
// High-performance batch DB fetch for sender attribution via Messages table mapping
|
||||
final mediaIds = mediaFiles.map((m) => m.mediaId).toList();
|
||||
final allMessages = await twonlyDB.messagesDao.getMessagesByMediaIds(
|
||||
mediaIds,
|
||||
|
|
@ -244,81 +257,24 @@ class MemoriesService {
|
|||
}
|
||||
}
|
||||
|
||||
for (final mediaFile in mediaFiles) {
|
||||
final mediaService = MediaFileService(mediaFile);
|
||||
if (!mediaService.imagePreviewAvailable) continue;
|
||||
|
||||
if (!mediaService.mediaFile.hasThumbnail &&
|
||||
mediaService.mediaFile.type != MediaType.audio) {
|
||||
unawaited(mediaService.createThumbnail());
|
||||
}
|
||||
|
||||
final senderContact = mediaIdToSenderContact[mediaFile.mediaId];
|
||||
final item = MemoryItem(
|
||||
mediaService: mediaService,
|
||||
messages: [],
|
||||
sender: senderContact,
|
||||
);
|
||||
|
||||
tempGalleryItems.add(item);
|
||||
|
||||
if (mediaFile.createdAt.month == now.month &&
|
||||
mediaFile.createdAt.day == now.day) {
|
||||
final diff = now.year - mediaFile.createdAt.year;
|
||||
if (diff > 0) {
|
||||
tempGalleryItemsLastYears.putIfAbsent(diff, () => []).add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort descending by creation date
|
||||
tempGalleryItems.sort(
|
||||
(a, b) => b.mediaService.mediaFile.createdAt.compareTo(
|
||||
a.mediaService.mediaFile.createdAt,
|
||||
),
|
||||
);
|
||||
|
||||
final tempOrderedByMonth = <String, List<int>>{};
|
||||
final tempMonths = <String>[];
|
||||
var lastMonth = '';
|
||||
|
||||
// High performance grouping leveraging pre-computed createdAtMonth column
|
||||
for (var i = 0; i < tempGalleryItems.length; i++) {
|
||||
final mFile = tempGalleryItems[i].mediaService.mediaFile;
|
||||
final month =
|
||||
mFile.createdAtMonth ??
|
||||
DateFormat('MMMM yyyy').format(mFile.createdAt);
|
||||
if (lastMonth != month) {
|
||||
lastMonth = month;
|
||||
tempMonths.add(month);
|
||||
}
|
||||
tempOrderedByMonth.putIfAbsent(month, () => []).add(i);
|
||||
}
|
||||
|
||||
for (final list in tempGalleryItemsLastYears.values) {
|
||||
list.sort(
|
||||
(a, b) => b.mediaService.mediaFile.createdAt.compareTo(
|
||||
a.mediaService.mediaFile.createdAt,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final sortedGalleryItemsLastYears =
|
||||
SplayTreeMap<int, List<MemoryItem>>.from(tempGalleryItemsLastYears);
|
||||
|
||||
final newState = MemoriesState(
|
||||
final newState = _computeState(
|
||||
mediaFiles: mediaFiles,
|
||||
mediaIdToSender: mediaIdToSenderContact,
|
||||
filesToMigrate: _currentState.filesToMigrate,
|
||||
galleryItems: tempGalleryItems,
|
||||
months: tempMonths,
|
||||
orderedByMonth: tempOrderedByMonth,
|
||||
galleryItemsLastYears: sortedGalleryItemsLastYears,
|
||||
);
|
||||
|
||||
for (final item in newState.galleryItems) {
|
||||
if (!item.mediaService.mediaFile.hasThumbnail &&
|
||||
item.mediaService.mediaFile.type != MediaType.audio) {
|
||||
unawaited(item.mediaService.createThumbnail());
|
||||
}
|
||||
}
|
||||
|
||||
_cachedState = newState;
|
||||
_updateStateWithObject(newState);
|
||||
_updateState(newState);
|
||||
|
||||
// Persist to KeyValueStore cache asynchronously
|
||||
final cacheList = tempGalleryItems
|
||||
final cacheList = newState.galleryItems
|
||||
.map(
|
||||
(item) => {
|
||||
'mediaId': item.mediaService.mediaFile.mediaId,
|
||||
|
|
@ -332,15 +288,17 @@ class MemoriesService {
|
|||
}
|
||||
}
|
||||
|
||||
void _updateStateWithObject(MemoriesState newState) {
|
||||
void _updateState(MemoriesState newState) {
|
||||
_currentState = newState;
|
||||
if (!_stateController.isClosed) {
|
||||
_stateController.add(_currentState);
|
||||
}
|
||||
_notifyState();
|
||||
}
|
||||
|
||||
void _updateState({int? filesToMigrate}) {
|
||||
void _updateMigrationCount(int filesToMigrate) {
|
||||
_currentState = _currentState.copyWith(filesToMigrate: filesToMigrate);
|
||||
_notifyState();
|
||||
}
|
||||
|
||||
void _notifyState() {
|
||||
if (!_stateController.isClosed) {
|
||||
_stateController.add(_currentState);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:go_router/go_router.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
|
@ -64,6 +65,22 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
|
|||
defaultAutoDownloadOptions;
|
||||
return ListView(
|
||||
children: [
|
||||
FutureBuilder<Map<MediaType, int>>(
|
||||
future: twonlyDB.mediaFilesDao.getStorageStats(),
|
||||
builder: (context, snapshot) {
|
||||
final stats = snapshot.data ?? {};
|
||||
final totalBytes = stats.values.fold<int>(0, (a, b) => a + b);
|
||||
final sizeStr = formatBytes(totalBytes);
|
||||
|
||||
return ListTile(
|
||||
title: Text(context.lang.settingsStorageManageTitle),
|
||||
subtitle: Text(sizeStr),
|
||||
onTap: () => context.push(Routes.settingsStorageManage),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
title: Text(context.lang.settingsStorageDataStoreInGTitle),
|
||||
subtitle: Text(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,155 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
class ManageStorageView extends StatefulWidget {
|
||||
const ManageStorageView({super.key});
|
||||
|
||||
@override
|
||||
State<ManageStorageView> createState() => _ManageStorageViewState();
|
||||
}
|
||||
|
||||
class _ManageStorageViewState extends State<ManageStorageView> {
|
||||
Map<MediaType, int> _stats = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadStats();
|
||||
}
|
||||
|
||||
Future<void> _loadStats() async {
|
||||
final stats = await twonlyDB.mediaFilesDao.getStorageStats();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_stats = stats;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final totalBytes = _stats.entries
|
||||
.where((e) => e.key != MediaType.audio)
|
||||
.fold<int>(0, (a, b) => a + b.value);
|
||||
final imageBytes = _stats[MediaType.image] ?? 0;
|
||||
final videoBytes = _stats[MediaType.video] ?? 0;
|
||||
final gifBytes = _stats[MediaType.gif] ?? 0;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.lang.settingsStorageManageTitle),
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
Text(
|
||||
context.lang.settingsStorageUsed,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
formatBytes(totalBytes),
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Container(
|
||||
height: 24,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (totalBytes == 0) return const SizedBox.shrink();
|
||||
|
||||
final maxWidth = constraints.maxWidth;
|
||||
final imageWidth = (imageBytes / totalBytes) * maxWidth;
|
||||
final videoWidth = (videoBytes / totalBytes) * maxWidth;
|
||||
final gifWidth = (gifBytes / totalBytes) * maxWidth;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
if (imageBytes > 0)
|
||||
Container(width: imageWidth, color: Colors.blue),
|
||||
if (videoBytes > 0)
|
||||
Container(width: videoWidth, color: Colors.green),
|
||||
if (gifBytes > 0)
|
||||
Container(width: gifWidth, color: Colors.orange),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_StorageCategoryTile(
|
||||
title: context.lang.settingsStorageImages,
|
||||
size: formatBytes(imageBytes),
|
||||
color: Colors.blue,
|
||||
),
|
||||
_StorageCategoryTile(
|
||||
title: context.lang.settingsStorageVideos,
|
||||
size: formatBytes(videoBytes),
|
||||
color: Colors.green,
|
||||
),
|
||||
_StorageCategoryTile(
|
||||
title: context.lang.settingsStorageGifs,
|
||||
size: formatBytes(gifBytes),
|
||||
color: Colors.orange,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StorageCategoryTile extends StatelessWidget {
|
||||
const _StorageCategoryTile({
|
||||
required this.title,
|
||||
required this.size,
|
||||
required this.color,
|
||||
});
|
||||
final String title;
|
||||
final String size;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
size,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue