feat: initial MemoriC project — spec, plan, POC (broker, agent, Android app)

This commit is contained in:
2026-05-13 16:06:58 +08:00
commit c083f89c66
150 changed files with 5891 additions and 0 deletions

136
poc/android/lib/main.dart Normal file
View 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),
};
}
}