Skip to content

feat: added ImageLibrary Section. #63

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jul 9, 2025
Merged
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
3 changes: 3 additions & 0 deletions android/app/src/debug/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
</manifest>
3 changes: 3 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<application
android:label="magic_epaper_app"
android:name="${applicationName}"
Expand Down
169 changes: 169 additions & 0 deletions lib/image_library/image_library.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import 'package:flutter/material.dart';
import 'package:magic_epaper_app/image_library/model/saved_image_model.dart';
import 'package:magic_epaper_app/image_library/provider/image_library_provider.dart';
import 'package:magic_epaper_app/image_library/services/image_operations_service.dart';
import 'package:magic_epaper_app/image_library/widgets/app_bar_widget.dart';
import 'package:magic_epaper_app/image_library/widgets/dialogs/batch_delete_confirmation_dialog.dart';
import 'package:magic_epaper_app/image_library/widgets/dialogs/delete_confirmation_dialog.dart';
import 'package:magic_epaper_app/image_library/widgets/empty_state_widget.dart';
import 'package:magic_epaper_app/image_library/widgets/image_grid_widget.dart';
import 'package:magic_epaper_app/image_library/widgets/dialogs/image_preview_dialog.dart';
import 'package:magic_epaper_app/image_library/widgets/search_and_filter_widget.dart';
import 'package:magic_epaper_app/constants/color_constants.dart';
import 'package:provider/provider.dart';

class ImageLibraryScreen extends StatefulWidget {
const ImageLibraryScreen({super.key});

@override
State<ImageLibraryScreen> createState() => _ImageLibraryScreenState();
}

class _ImageLibraryScreenState extends State<ImageLibraryScreen> {
final TextEditingController _searchController = TextEditingController();
bool _isDeleteMode = false;
Set<String> _selectedImages = <String>{};
late ImageOperationsService _operationsService;

@override
void initState() {
super.initState();
_operationsService = ImageOperationsService(context);
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<ImageLibraryProvider>().loadSavedImages();
});
}

@override
void dispose() {
_searchController.dispose();
super.dispose();
}

void _handleImageTap(SavedImage image) {
if (_isDeleteMode) {
_toggleImageSelection(image.id);
} else {
_showImagePreview(image);
}
}

void _toggleImageSelection(String imageId) {
setState(() {
if (_selectedImages.contains(imageId)) {
_selectedImages.remove(imageId);
} else {
_selectedImages.add(imageId);
}
});
}

void _showImagePreview(SavedImage image) {
final provider = context.read<ImageLibraryProvider>();

showDialog(
context: context,
builder: (context) => ImagePreviewDialog(
image: image,
epd: _operationsService.getEpdFromImage(image),
onDelete: () => _showDeleteDialog(image, provider),
onRename: (newName) =>
_operationsService.renameImage(image, newName, provider),
onTransfer: () => _operationsService.transferSingleImage(image),
),
);
}

void _showDeleteDialog(SavedImage image, ImageLibraryProvider provider) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => DeleteConfirmationDialog(
image: image,
onConfirm: () => _operationsService.deleteImage(image, provider),
),
);
}

void _showBatchDeleteDialog() {
final provider = context.read<ImageLibraryProvider>();
final selectedImageObjects = provider.savedImages
.where((image) => _selectedImages.contains(image.id))
.toList();

showDialog(
context: context,
barrierDismissible: false,
builder: (context) => BatchDeleteConfirmationDialog(
selectedImages: selectedImageObjects,
onConfirm: () => _performBatchDelete(selectedImageObjects, provider),
),
);
}

Future<void> _performBatchDelete(
List<SavedImage> selectedImages,
ImageLibraryProvider provider,
) async {
await _operationsService.batchDeleteImages(selectedImages, provider);
_exitDeleteMode();
}

void _exitDeleteMode() {
setState(() {
_isDeleteMode = false;
_selectedImages.clear();
});
}

void _enterDeleteMode() {
setState(() {
_isDeleteMode = true;
_selectedImages.clear();
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: LibraryAppBar(
isDeleteMode: _isDeleteMode,
selectedCount: _selectedImages.length,
onDeletePressed: _showBatchDeleteDialog,
onExitDeleteMode: _exitDeleteMode,
onEnterDeleteMode: _enterDeleteMode,
),
body: Consumer<ImageLibraryProvider>(
builder: (context, provider, child) {
if (provider.isLoading) {
return const Center(
child: CircularProgressIndicator(color: colorAccent),
);
}

if (provider.savedImages.isEmpty) {
return const EmptyStateWidget();
}

return Column(
children: [
SearchAndFilterWidget(
searchController: _searchController,
provider: provider,
),
Expanded(
child: ImageGridWidget(
images: provider.filteredImages,
isDeleteMode: _isDeleteMode,
selectedImages: _selectedImages,
onImageTap: _handleImageTap,
),
),
],
);
},
),
);
}
}
38 changes: 38 additions & 0 deletions lib/image_library/model/image_properties.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
class ImageProperties {
final int fileSizeBytes;
final int width;
final int height;
final String format;
final double aspectRatio;
final DateTime lastModified;
final String filePath;

ImageProperties({
required this.fileSizeBytes,
required this.width,
required this.height,
required this.format,
required this.aspectRatio,
required this.lastModified,
required this.filePath,
});

String get fileSizeFormatted {
if (fileSizeBytes < 1024) {
return '$fileSizeBytes B';
} else if (fileSizeBytes < 1024 * 1024) {
return '${(fileSizeBytes / 1024).toStringAsFixed(1)} KB';
} else {
return '${(fileSizeBytes / (1024 * 1024)).toStringAsFixed(1)} MB';
}
}

String get resolution => '${width} × ${height}';

String get megapixels {
final mp = (width * height) / 1000000;
return '${mp.toStringAsFixed(1)} MP';
}

String get fileName => filePath.split('/').last;
}
65 changes: 65 additions & 0 deletions lib/image_library/model/saved_image_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';

class SavedImage {
final String id;
final String name;
final String filePath;
final DateTime createdAt;
final String source;
final Map<String, dynamic>? metadata;

SavedImage({
required this.id,
required this.name,
required this.filePath,
required this.createdAt,
required this.source,
this.metadata,
});

Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'filePath': filePath,
'createdAt': createdAt.toIso8601String(),
'source': source,
'metadata': metadata,
};
}

factory SavedImage.fromJson(Map<String, dynamic> json) {
return SavedImage(
id: json['id'],
name: json['name'],
filePath: json['filePath'],
createdAt: DateTime.parse(json['createdAt']),
source: json['source'],
metadata: json['metadata'],
);
}

Future<Uint8List?> getImageData() async {
try {
final file = File(filePath);
if (await file.exists()) {
return await file.readAsBytes();
}
return null;
} catch (e) {
debugPrint('Error reading image file: $e');
return null;
}
}

Future<bool> fileExists() async {
try {
final file = File(filePath);
return await file.exists();
} catch (e) {
return false;
}
}
}
Loading