Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 89 additions & 18 deletions apps/flites/lib/core/app_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ class AppState {
/// The main project data containing all image rows and project metadata
final _projectData = signal<FlitesImageMap>(
FlitesImageMap(
rows: [
FlitesImageRow(images: [], name: 'Animation 1'),
],
rows: [FlitesImageRow(images: [], name: 'Animation 1')],
),
);

Expand Down Expand Up @@ -59,9 +57,8 @@ class AppState {
String? get selectedImageId => _selectedImageId.value;

/// Currently selected animation (computed from selected row)
ReadonlySignal<FlitesImageRow> get selectedAnimation => computed(
() => _projectData.value.rows[_selectedRowIndex.value],
);
ReadonlySignal<FlitesImageRow> get selectedAnimation =>
computed(() => _projectData.value.rows[_selectedRowIndex.value]);

/// Sets the selected image row
void setSelectedRow(int index) {
Expand Down Expand Up @@ -108,6 +105,81 @@ class AppState {
_canvasPosition.value += offset;
}

void reAlignAllImages() {
final center = _centerOfAllImages();

if (center == null) {
return;
}

for (final image in _getCurrentRow().images) {
final offsetFromCenter = image.center - center;
final newPosition = image.positionOnCanvas - offsetFromCenter;

saveImageChanges(image.copyWith(positionOnCanvas: newPosition));
}

centerCanvasOnAllImages();
}

// Centers the canvas around a single image.
void centerCanvasOnImage(String imageId) {
final currentRow = _getCurrentRow();
final index = currentRow.images.indexWhere((image) => image.id == imageId);

if (index == -1) {
return;
}
final targetImage = currentRow.images[index];

_recenterCanvas(targetImage.center);
}

// Centers the canvas on the average of the currently selected images.
void centerCanvasOnAllImages() {
final center = _centerOfAllImages();

if (center == null) {
return;
}

_recenterCanvas(center);
}

// The non-scaled center point of all images
Offset? _centerOfAllImages() {
// Calculate center of currently selected image row.
final centersOfRow = _getCurrentRow().images
.map((image) => image.center)
.toList();
if (centersOfRow.isEmpty) {
return null;
}

// The average pixel center of the images in the currently selected row
//(in our coordinate system).
final averageCenter =
centersOfRow.reduce((a, b) => a + b) / centersOfRow.length.toDouble();

return averageCenter;
}

// Re-centers the canvas around a non-scaled center point.
void _recenterCanvas(Offset unscaledTargetCenter) {
// Apply scaling factor to the average center (which is absolute).
final scaledImageCenter = unscaledTargetCenter * _canvasScalingFactor.value;

// The center of the canvas from a window size perspective.
// This changes with window resizes
final canvasPixelCenter = Offset(
_canvasSizePixel.value.width / 2,
_canvasSizePixel.value.height / 2,
);

// Update the position of the canvas to center the images in the window
_canvasPosition.value = canvasPixelCenter - scaledImageCenter;
}

/// Updates canvas size
void updateCanvasSize(Size size) {
if (size != _canvasSizePixel.value) {
Expand Down Expand Up @@ -142,10 +214,7 @@ class AppState {
/// Adds a new image row
void addImageRow(String name) {
final currentRows = _projectData.value.rows;
final newRows = [
...currentRows,
FlitesImageRow(images: [], name: name),
];
final newRows = [...currentRows, FlitesImageRow(images: [], name: name)];
_projectData.value = _projectData.value.copyWith(rows: newRows);
}

Expand Down Expand Up @@ -207,9 +276,7 @@ class AppState {
final currentRow = _getCurrentRow();
final newRow = currentRow.copyWith(
images: currentRow.images
.map(
(img) => img.id == image.id ? image : img,
)
.map((img) => img.id == image.id ? image : img)
.toList(),
);
_updateCurrentRow(newRow);
Expand All @@ -218,8 +285,9 @@ class AppState {
/// Deletes an image
void deleteImage(String imageId) {
final currentRow = _getCurrentRow();
final updatedImages =
currentRow.images.where((image) => image.id != imageId).toList();
final updatedImages = currentRow.images
.where((image) => image.id != imageId)
.toList();

final updatedRow = currentRow.copyWith(images: updatedImages);
_updateCurrentRow(updatedRow);
Expand Down Expand Up @@ -303,14 +371,17 @@ class AppState {
/// Renames images according to their order
void renameImagesAccordingToOrder() {
final currentRow = _getCurrentRow();
final baseName =
currentRow.name.toLowerCase().replaceAll(RegExp(r'\s+'), '_');
final baseName = currentRow.name.toLowerCase().replaceAll(
RegExp(r'\s+'),
'_',
);

final newImages = currentRow.images.asMap().entries.map((entry) {
final index = entry.key;
final originalImage = entry.value;

final fileExtension = originalImage.originalName?.endsWith('.svg') ?? false
final fileExtension =
originalImage.originalName?.endsWith('.svg') ?? false
? '.svg'
: '.png';

Expand Down
44 changes: 28 additions & 16 deletions apps/flites/lib/ui/panel/controls/panel_list_item.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';

import '../../../constants/app_sizes.dart';
import '../../../core/app_state.dart';
import '../../../types/flites_image.dart';
import '../../../types/secondary_click_context_data.dart';
import '../../../utils/svg_utils.dart';
Expand Down Expand Up @@ -32,7 +33,8 @@ class PanelListItem extends StatefulWidget {
final List<IconBtn> Function({
required bool isHovered,
required bool isActive,
})? actionButtons;
})?
actionButtons;
final bool isSelected;
final VoidCallback? onTap;
final bool hoverSelected;
Expand All @@ -45,24 +47,23 @@ class PanelListItem extends StatefulWidget {
bool? isSelected,
VoidCallback? onTap,
List<IconBtn> Function({required bool isHovered, required bool isActive})?
actionButtons,
actionButtons,
String? title,
String? subtitle,
FlitesImage? image,
String? value,
IconData? icon,
}) =>
PanelListItem(
title: title ?? this.title,
subtitle: subtitle ?? this.subtitle,
image: image ?? this.image,
actionButtons: actionButtons ?? this.actionButtons,
isSelected: isSelected ?? this.isSelected,
onTap: onTap ?? this.onTap,
hoverSelected: hoverSelected ?? this.hoverSelected,
value: value ?? this.value,
icon: icon ?? this.icon,
);
}) => PanelListItem(
title: title ?? this.title,
subtitle: subtitle ?? this.subtitle,
image: image ?? this.image,
actionButtons: actionButtons ?? this.actionButtons,
isSelected: isSelected ?? this.isSelected,
onTap: onTap ?? this.onTap,
hoverSelected: hoverSelected ?? this.hoverSelected,
value: value ?? this.value,
icon: icon ?? this.icon,
);
}

class _PanelListItemState extends State<PanelListItem> {
Expand All @@ -73,15 +74,26 @@ class _PanelListItemState extends State<PanelListItem> {
final theme = Theme.of(context);

final actionButtonList =
widget.actionButtons
?.call(isHovered: isHovered, isActive: widget.isSelected) ??
widget.actionButtons?.call(
isHovered: isHovered,
isActive: widget.isSelected,
) ??
[];

return RightClickableItemWrapper(
contextData: SecondaryClickContextData(copyableData: [widget.image]),
child: ClipRRect(
borderRadius: BorderRadius.circular(borderRadiusMd),
child: HoverBtn(
onDoubleTap: () {
final imageId = widget.image?.id;

if (imageId == null) {
return;
}

appState.centerCanvasOnImage(imageId);
},
hoverColor: theme.colorScheme.surfaceContainerLow,
customBorder: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
Expand Down
11 changes: 5 additions & 6 deletions apps/flites/lib/utils/flites_image_factory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,13 @@ class FlitesImageFactory {
// Extract image data
// Sort images by frame number (for GIFs)
images.sort((a, b) {
final numA = int.tryParse(
final numA =
int.tryParse(
RegExp(r'frame_(\d+)').firstMatch(a.name ?? '')?.group(1) ?? '0',
) ??
0;
final numB = int.tryParse(
final numB =
int.tryParse(
RegExp(r'frame_(\d+)').firstMatch(b.name ?? '')?.group(1) ?? '0',
) ??
0;
Expand All @@ -171,10 +173,7 @@ class FlitesImageFactory {
final result = images
.map((img) {
try {
return FlitesImage.scaled(
img.image!,
originalName: img.name,
);
return FlitesImage.scaled(img.image!, originalName: img.name);
} on Exception {
// Skip images that fail to process
return null;
Expand Down
25 changes: 15 additions & 10 deletions apps/flites/lib/utils/png_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@ import 'package:flutter/services.dart';
class PngUtils {
// Private constructor to prevent instantiation
PngUtils._();
static Size getSizeOfPng(ByteData bytes) => Size(
bytes.getUint32(16).toDouble(),
bytes.getUint32(20).toDouble(),
);
static Size getSizeOfPng(ByteData bytes) =>
Size(bytes.getUint32(16).toDouble(), bytes.getUint32(20).toDouble());

static bool isPng(Uint8List data) {
// PNG signature
const List<int> pngSignature = [
0x89,
0x50,
Expand All @@ -21,10 +18,18 @@ class PngUtils {
0x0A,
];

// Check if data has enough length and starts with the PNG signature
return data.length >= 8 &&
data
.sublist(0, 8)
.every((byte) => byte == pngSignature[data.indexOf(byte)]);
if (data.length < 8) {
return false;
}

// Compare index-to-index directly
for (int i = 0; i < 8; i++) {
if (data[i] != pngSignature[i]) {
// TODO: we could show a toast here "Invalid PNG data."
return false;
}
}

return true;
}
}
Loading