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,55 +45,82 @@ class ChatAudioEntry extends StatelessWidget {
minWidth, minWidth,
); );
return Container( return LayoutBuilder(
constraints: BoxConstraints( builder: (context, constraints) {
maxWidth: MediaQuery.of(context).size.width * 0.8, final textWidth = measureTextWidth(info.text);
minWidth: 250, const timeWidth = 60.0;
), final isExpanded =
padding: const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10), info.expanded ||
decoration: BoxDecoration( (textWidth + timeWidth + 20 > constraints.maxWidth);
color: info.color, final effectiveSpacerWidth =
borderRadius: borderRadius, constraints.minWidth - textWidth - timeWidth;
), final spacerWidth = effectiveSpacerWidth > 0
child: Column( ? effectiveSpacerWidth
crossAxisAlignment: CrossAxisAlignment.start, : 0.0;
children: [
if (info.displayUserName != '') return Container(
Text( constraints: BoxConstraints(
info.displayUserName, maxWidth: MediaQuery.of(context).size.width * 0.8,
textAlign: TextAlign.left, minWidth: 250,
style: const TextStyle( ),
color: Colors.white, padding: const EdgeInsets.only(
fontWeight: FontWeight.bold, left: 10,
), top: 6,
), bottom: 6,
Row( right: 10,
mainAxisSize: MainAxisSize.min, ),
crossAxisAlignment: CrossAxisAlignment.end, decoration: BoxDecoration(
color: info.color,
borderRadius: borderRadius,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (info.text != '') if (info.displayUserName != '')
Expanded( Text(
child: BetterText(text: info.text, textColor: info.textColor), info.displayUserName,
) textAlign: TextAlign.left,
else ...[ style: const TextStyle(
if (mediaService.mediaFile.downloadState == color: Colors.white,
DownloadState.ready || fontWeight: FontWeight.bold,
mediaService.mediaFile.downloadState == null) ),
mediaService.tempPath.existsSync() ),
? InChatAudioPlayer( Row(
path: mediaService.tempPath.path, mainAxisSize: MainAxisSize.min,
message: message, crossAxisAlignment: CrossAxisAlignment.end,
) children: [
: Container() if (isExpanded && info.text != '')
else Expanded(
MessageSendStateIcon([message], [mediaService.mediaFile]), child: BetterText(
], text: info.text,
if (info.displayTime || message.modifiedAt != null) textColor: info.textColor,
FriendlyMessageTime(message: message), ),
)
else if (info.text != '') ...[
BetterText(text: info.text, textColor: info.textColor),
SizedBox(width: spacerWidth),
] else ...[
if (mediaService.mediaFile.downloadState ==
DownloadState.ready ||
mediaService.mediaFile.downloadState == null)
mediaService.tempPath.existsSync()
? InChatAudioPlayer(
path: mediaService.tempPath.path,
message: message,
)
: Container()
else
MessageSendStateIcon([message], [mediaService.mediaFile]),
SizedBox(width: spacerWidth),
],
if (info.displayTime || message.modifiedAt != null)
FriendlyMessageTime(message: message),
],
),
], ],
), ),
], );
), },
); );
} }
} }

View file

@ -49,48 +49,71 @@ class ChatTextEntry extends StatelessWidget {
minWidth, minWidth,
); );
return Container( return LayoutBuilder(
constraints: BoxConstraints( builder: (context, constraints) {
maxWidth: MediaQuery.of(context).size.width * 0.8, final textWidth = measureTextWidth(info.text);
minWidth: minWidth, const timeWidth = 60.0;
), final isExpanded =
padding: const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10), info.expanded ||
decoration: BoxDecoration( (textWidth + timeWidth + 20 > constraints.maxWidth);
color: info.color, final effectiveSpacerWidth =
borderRadius: borderRadius, constraints.minWidth - textWidth - timeWidth;
), final spacerWidth = effectiveSpacerWidth > 0
child: Column( ? effectiveSpacerWidth
crossAxisAlignment: CrossAxisAlignment.start, : 0.0;
children: [
if (info.displayUserName != '') return Container(
Text( constraints: BoxConstraints(
info.displayUserName, maxWidth: MediaQuery.of(context).size.width * 0.8,
textAlign: TextAlign.left, minWidth: minWidth,
style: const TextStyle( ),
color: Colors.white, padding: const EdgeInsets.only(
fontWeight: FontWeight.bold, left: 10,
), top: 6,
), bottom: 6,
Row( right: 10,
mainAxisSize: MainAxisSize.min, ),
crossAxisAlignment: CrossAxisAlignment.end, decoration: BoxDecoration(
color: info.color,
borderRadius: borderRadius,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (info.expanded) if (info.displayUserName != '')
Expanded( Text(
child: BetterText(text: info.text, textColor: info.textColor), info.displayUserName,
) textAlign: TextAlign.left,
else ...[ style: const TextStyle(
BetterText(text: info.text, textColor: info.textColor), color: Colors.white,
SizedBox( fontWeight: FontWeight.bold,
width: info.spacerWidth, ),
), ),
], Row(
if (info.displayTime || message.modifiedAt != null) mainAxisSize: MainAxisSize.min,
FriendlyMessageTime(message: message), crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (isExpanded)
Expanded(
child: BetterText(
text: info.text,
textColor: info.textColor,
),
)
else ...[
BetterText(text: info.text, textColor: info.textColor),
SizedBox(
width: spacerWidth,
),
],
if (info.displayTime || message.modifiedAt != null)
FriendlyMessageTime(message: message),
],
),
], ],
), ),
], );
), },
); );
} }
} }

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