feat: initial MemoriC project — spec, plan, POC (broker, agent, Android app)
This commit is contained in:
136
poc/android/lib/main.dart
Normal file
136
poc/android/lib/main.dart
Normal file
@@ -0,0 +1,136 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
const brokerUrl = 'https://memory.clouderized.com';
|
||||
|
||||
void main() => runApp(const MemoryApp());
|
||||
|
||||
class MemoryApp extends StatelessWidget {
|
||||
const MemoryApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Memory by Clouderized',
|
||||
theme: ThemeData(colorSchemeSeed: const Color(0xFF0F766E)),
|
||||
home: const UploadPage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum UploadStatus { pending, uploading, done, failed }
|
||||
|
||||
class PhotoItem {
|
||||
final File file;
|
||||
UploadStatus status;
|
||||
PhotoItem(this.file, {this.status = UploadStatus.pending});
|
||||
}
|
||||
|
||||
class UploadPage extends StatefulWidget {
|
||||
const UploadPage({super.key});
|
||||
|
||||
@override
|
||||
State<UploadPage> createState() => _UploadPageState();
|
||||
}
|
||||
|
||||
class _UploadPageState extends State<UploadPage> {
|
||||
final _picker = ImagePicker();
|
||||
final List<PhotoItem> _photos = [];
|
||||
|
||||
Future<void> _pickPhotos() async {
|
||||
final picked = await _picker.pickMultiImage();
|
||||
if (picked.isEmpty) return;
|
||||
setState(() {
|
||||
for (final x in picked) {
|
||||
_photos.add(PhotoItem(File(x.path)));
|
||||
}
|
||||
});
|
||||
_uploadAll();
|
||||
}
|
||||
|
||||
Future<void> _uploadAll() async {
|
||||
for (final item in _photos.where((i) => i.status == UploadStatus.pending)) {
|
||||
setState(() => item.status = UploadStatus.uploading);
|
||||
try {
|
||||
final filename = p.basename(item.file.path);
|
||||
final req = http.MultipartRequest(
|
||||
'POST',
|
||||
Uri.parse('$brokerUrl/upload/$filename'),
|
||||
);
|
||||
req.files.add(await http.MultipartFile.fromPath('file', item.file.path));
|
||||
final res = await req.send();
|
||||
setState(() {
|
||||
item.status = res.statusCode == 200 ? UploadStatus.done : UploadStatus.failed;
|
||||
});
|
||||
} catch (_) {
|
||||
setState(() => item.status = UploadStatus.failed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Memory by Clouderized'),
|
||||
backgroundColor: const Color(0xFF0F766E),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
body: _photos.isEmpty
|
||||
? const Center(child: Text('No photos selected yet.'))
|
||||
: GridView.builder(
|
||||
padding: const EdgeInsets.all(8),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
crossAxisSpacing: 4,
|
||||
mainAxisSpacing: 4,
|
||||
),
|
||||
itemCount: _photos.length,
|
||||
itemBuilder: (_, i) => _PhotoTile(item: _photos[i]),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: _pickPhotos,
|
||||
backgroundColor: const Color(0xFF0F766E),
|
||||
foregroundColor: Colors.white,
|
||||
icon: const Icon(Icons.add_photo_alternate),
|
||||
label: const Text('Pick Photos'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PhotoTile extends StatelessWidget {
|
||||
final PhotoItem item;
|
||||
const _PhotoTile({required this.item});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Image.file(item.file, fit: BoxFit.cover),
|
||||
Positioned(
|
||||
bottom: 4,
|
||||
right: 4,
|
||||
child: _statusBadge(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _statusBadge() {
|
||||
return switch (item.status) {
|
||||
UploadStatus.pending => const Icon(Icons.schedule, color: Colors.white, size: 20),
|
||||
UploadStatus.uploading => const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
|
||||
),
|
||||
UploadStatus.done => const Icon(Icons.check_circle, color: Colors.greenAccent, size: 20),
|
||||
UploadStatus.failed => const Icon(Icons.error, color: Colors.redAccent, size: 20),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user