smaller ui fixes

This commit is contained in:
otsmr 2026-05-16 19:13:42 +02:00
parent 91eedc76b0
commit 68c99c271f
6 changed files with 187 additions and 129 deletions

View file

@ -47,21 +47,15 @@ Future<bool> createThumbnailsForImage(
) async { ) async {
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
if (destinationFile.existsSync()) {
return true;
}
try { try {
final bytes = sourceFile.readAsBytesSync(); await FlutterImageCompress.compressAndGetFile(
final result = await FlutterImageCompress.compressWithList( sourceFile.absolute.path,
bytes, destinationFile.absolute.path,
minWidth: 200, minWidth: 300,
minHeight: 200, minHeight: 300,
quality: 70, quality: 100,
format: CompressFormat.webp, format: CompressFormat.webp,
); );
destinationFile.writeAsBytesSync(result);
stopwatch.stop(); stopwatch.stop();
Log.info( Log.info(
'It took ${stopwatch.elapsedMilliseconds}ms to create the image thumbnail.', 'It took ${stopwatch.elapsedMilliseconds}ms to create the image thumbnail.',
@ -94,15 +88,15 @@ Future<bool> createThumbnailsForGif(
final thumbnail = img.copyResize( final thumbnail = img.copyResize(
image, image,
width: image.width > image.height ? 200 : null, width: image.width > image.height ? 400 : null,
height: image.height >= image.width ? 200 : null, height: image.height >= image.width ? 400 : null,
); );
final pngBytes = img.encodePng(thumbnail); final pngBytes = img.encodePng(thumbnail);
final webp = await FlutterImageCompress.compressWithList( final webp = await FlutterImageCompress.compressWithList(
pngBytes, pngBytes,
format: CompressFormat.webp, format: CompressFormat.webp,
quality: 70, quality: 85,
); );
destinationFile.writeAsBytesSync(webp); destinationFile.writeAsBytesSync(webp);

View file

@ -15,6 +15,7 @@ import 'package:twonly/src/utils/log.dart';
class MemoriesState { class MemoriesState {
const MemoriesState({ const MemoriesState({
required this.filesToMigrate, required this.filesToMigrate,
required this.totalFilesToMigrate,
required this.galleryItems, required this.galleryItems,
required this.months, required this.months,
required this.orderedByMonth, required this.orderedByMonth,
@ -22,16 +23,21 @@ class MemoriesState {
}); });
final int filesToMigrate; final int filesToMigrate;
final int totalFilesToMigrate;
final List<MemoryItem> galleryItems; final List<MemoryItem> galleryItems;
final List<String> months; final List<String> months;
final Map<String, List<int>> orderedByMonth; final Map<String, List<int>> orderedByMonth;
final Map<int, List<MemoryItem>> galleryItemsLastYears; final Map<int, List<MemoryItem>> galleryItemsLastYears;
bool get isLoading => filesToMigrate > 0; bool get isLoading => filesToMigrate > 0;
double get migrationProgress => totalFilesToMigrate > 0
? (totalFilesToMigrate - filesToMigrate) / totalFilesToMigrate
: 0;
bool get isEmpty => galleryItems.isEmpty && filesToMigrate == 0; bool get isEmpty => galleryItems.isEmpty && filesToMigrate == 0;
MemoriesState copyWith({ MemoriesState copyWith({
int? filesToMigrate, int? filesToMigrate,
int? totalFilesToMigrate,
List<MemoryItem>? galleryItems, List<MemoryItem>? galleryItems,
List<String>? months, List<String>? months,
Map<String, List<int>>? orderedByMonth, Map<String, List<int>>? orderedByMonth,
@ -39,6 +45,7 @@ class MemoriesState {
}) { }) {
return MemoriesState( return MemoriesState(
filesToMigrate: filesToMigrate ?? this.filesToMigrate, filesToMigrate: filesToMigrate ?? this.filesToMigrate,
totalFilesToMigrate: totalFilesToMigrate ?? this.totalFilesToMigrate,
galleryItems: galleryItems ?? this.galleryItems, galleryItems: galleryItems ?? this.galleryItems,
months: months ?? this.months, months: months ?? this.months,
orderedByMonth: orderedByMonth ?? this.orderedByMonth, orderedByMonth: orderedByMonth ?? this.orderedByMonth,
@ -63,6 +70,7 @@ class MemoriesService {
MemoriesState _currentState = const MemoriesState( MemoriesState _currentState = const MemoriesState(
filesToMigrate: 0, filesToMigrate: 0,
totalFilesToMigrate: 0,
galleryItems: [], galleryItems: [],
months: [], months: [],
orderedByMonth: {}, orderedByMonth: {},
@ -182,6 +190,7 @@ class MemoriesService {
return MemoriesState( return MemoriesState(
filesToMigrate: filesToMigrate, filesToMigrate: filesToMigrate,
totalFilesToMigrate: filesToMigrate, // Reset total when computing new state? No, keep existing total if migrating.
galleryItems: tempGalleryItems, galleryItems: tempGalleryItems,
months: tempMonths, months: tempMonths,
orderedByMonth: tempOrderedByMonth, orderedByMonth: tempOrderedByMonth,
@ -195,7 +204,11 @@ class MemoriesService {
.getAllMediaFilesPendingMigration(); .getAllMediaFilesPendingMigration();
if (pendingFiles.isNotEmpty) { if (pendingFiles.isNotEmpty) {
_updateMigrationCount(pendingFiles.length); _currentState = _currentState.copyWith(
filesToMigrate: pendingFiles.length,
totalFilesToMigrate: pendingFiles.length,
);
_notifyState();
for (final mediaFile in pendingFiles) { for (final mediaFile in pendingFiles) {
final mediaService = MediaFileService(mediaFile); final mediaService = MediaFileService(mediaFile);
@ -261,7 +274,7 @@ class MemoriesService {
mediaFiles: mediaFiles, mediaFiles: mediaFiles,
mediaIdToSender: mediaIdToSenderContact, mediaIdToSender: mediaIdToSenderContact,
filesToMigrate: _currentState.filesToMigrate, filesToMigrate: _currentState.filesToMigrate,
); ).copyWith(totalFilesToMigrate: _currentState.totalFilesToMigrate);
for (final item in newState.galleryItems) { for (final item in newState.galleryItems) {
if (!item.mediaService.mediaFile.hasThumbnail && if (!item.mediaService.mediaFile.hasThumbnail &&

View file

@ -45,12 +45,30 @@ class ChatAudioEntry extends StatelessWidget {
minWidth, minWidth,
); );
return LayoutBuilder(
builder: (context, constraints) {
final textWidth = measureTextWidth(info.text);
const timeWidth = 60.0;
final isExpanded =
info.expanded ||
(textWidth + timeWidth + 20 > constraints.maxWidth);
final effectiveSpacerWidth =
constraints.minWidth - textWidth - timeWidth;
final spacerWidth = effectiveSpacerWidth > 0
? effectiveSpacerWidth
: 0.0;
return Container( return Container(
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.8, maxWidth: MediaQuery.of(context).size.width * 0.8,
minWidth: 250, minWidth: 250,
), ),
padding: const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10), padding: const EdgeInsets.only(
left: 10,
top: 6,
bottom: 6,
right: 10,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: info.color, color: info.color,
borderRadius: borderRadius, borderRadius: borderRadius,
@ -71,11 +89,17 @@ class ChatAudioEntry extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
if (info.text != '') if (isExpanded && info.text != '')
Expanded( Expanded(
child: BetterText(text: info.text, textColor: info.textColor), child: BetterText(
text: info.text,
textColor: info.textColor,
),
) )
else ...[ else if (info.text != '') ...[
BetterText(text: info.text, textColor: info.textColor),
SizedBox(width: spacerWidth),
] else ...[
if (mediaService.mediaFile.downloadState == if (mediaService.mediaFile.downloadState ==
DownloadState.ready || DownloadState.ready ||
mediaService.mediaFile.downloadState == null) mediaService.mediaFile.downloadState == null)
@ -87,6 +111,7 @@ class ChatAudioEntry extends StatelessWidget {
: Container() : Container()
else else
MessageSendStateIcon([message], [mediaService.mediaFile]), MessageSendStateIcon([message], [mediaService.mediaFile]),
SizedBox(width: spacerWidth),
], ],
if (info.displayTime || message.modifiedAt != null) if (info.displayTime || message.modifiedAt != null)
FriendlyMessageTime(message: message), FriendlyMessageTime(message: message),
@ -95,6 +120,8 @@ class ChatAudioEntry extends StatelessWidget {
], ],
), ),
); );
},
);
} }
} }

View file

@ -49,12 +49,30 @@ class ChatTextEntry extends StatelessWidget {
minWidth, minWidth,
); );
return LayoutBuilder(
builder: (context, constraints) {
final textWidth = measureTextWidth(info.text);
const timeWidth = 60.0;
final isExpanded =
info.expanded ||
(textWidth + timeWidth + 20 > constraints.maxWidth);
final effectiveSpacerWidth =
constraints.minWidth - textWidth - timeWidth;
final spacerWidth = effectiveSpacerWidth > 0
? effectiveSpacerWidth
: 0.0;
return Container( return Container(
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.8, maxWidth: MediaQuery.of(context).size.width * 0.8,
minWidth: minWidth, minWidth: minWidth,
), ),
padding: const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10), padding: const EdgeInsets.only(
left: 10,
top: 6,
bottom: 6,
right: 10,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: info.color, color: info.color,
borderRadius: borderRadius, borderRadius: borderRadius,
@ -75,14 +93,17 @@ class ChatTextEntry extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
if (info.expanded) if (isExpanded)
Expanded( Expanded(
child: BetterText(text: info.text, textColor: info.textColor), child: BetterText(
text: info.text,
textColor: info.textColor,
),
) )
else ...[ else ...[
BetterText(text: info.text, textColor: info.textColor), BetterText(text: info.text, textColor: info.textColor),
SizedBox( SizedBox(
width: info.spacerWidth, width: spacerWidth,
), ),
], ],
if (info.displayTime || message.modifiedAt != null) if (info.displayTime || message.modifiedAt != null)
@ -92,5 +113,7 @@ class ChatTextEntry extends StatelessWidget {
], ],
), ),
); );
},
);
} }
} }

View file

@ -50,10 +50,11 @@ BubbleInfo getBubbleInfo(
info.spacerWidth = minWidth - measureTextWidth(info.text) - 53; info.spacerWidth = minWidth - measureTextWidth(info.text) - 53;
if (info.spacerWidth < 0) info.spacerWidth = 0; if (info.spacerWidth < 0) info.spacerWidth = 0;
info.expanded = false; info
if (message.quotesMessageId == null) { ..expanded = false
info.color = getMessageColor(message.senderId != null); ..color = message.quotesMessageId != null
} ? Colors.transparent
: getMessageColor(message.senderId != null);
if (message.isDeletedFromSender) { if (message.isDeletedFromSender) {
info info
..color = context.color.surfaceBright ..color = context.color.surfaceBright

View file

@ -9,7 +9,6 @@ import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart';
import 'package:twonly/src/visual/components/draggable_scrollbar.comp.dart'; import 'package:twonly/src/visual/components/draggable_scrollbar.comp.dart';
import 'package:twonly/src/visual/components/snackbar.dart'; import 'package:twonly/src/visual/components/snackbar.dart';
import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart';
import 'package:twonly/src/visual/views/memories/components/flashback_banner.comp.dart'; import 'package:twonly/src/visual/views/memories/components/flashback_banner.comp.dart';
import 'package:twonly/src/visual/views/memories/components/memory_thumbnail.comp.dart'; import 'package:twonly/src/visual/views/memories/components/memory_thumbnail.comp.dart';
import 'package:twonly/src/visual/views/memories/components/selection_toolbar.comp.dart'; import 'package:twonly/src/visual/views/memories/components/selection_toolbar.comp.dart';
@ -293,29 +292,6 @@ class MemoriesViewState extends State<MemoriesView> {
builder: (context, snapshot) { builder: (context, snapshot) {
final state = snapshot.data ?? _service.currentState; final state = snapshot.data ?? _service.currentState;
if (state.isLoading) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ThreeRotatingDots(
size: 40,
color: context.color.primary,
),
const SizedBox(height: 16),
Text(
context.lang.migrationOfMemories(state.filesToMigrate),
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
if (state.isEmpty) { if (state.isEmpty) {
return Center( return Center(
child: Padding( child: Padding(
@ -417,6 +393,30 @@ class MemoriesViewState extends State<MemoriesView> {
elevation: 0, elevation: 0,
backgroundColor: context.color.surface, backgroundColor: context.color.surface,
actions: [ actions: [
if (state.isLoading)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
),
child: Center(
child: Tooltip(
message: context.lang.migrationOfMemories(
state.filesToMigrate,
),
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
value: state.migrationProgress,
strokeWidth: 2.5,
color: context.color.primary,
backgroundColor: context.color.primary
.withOpacity(0.2),
),
),
),
),
),
IconButton( IconButton(
icon: Icon( icon: Icon(
_filterFavoritesOnly _filterFavoritesOnly