Implement Phase 1 Android client: Flutter share receiver + upload service
- Register ACTION_SEND IntentFilter for image/* in AndroidManifest - Kotlin MethodChannel bridge: extract image URI, original filename, resolve to file - Flutter bottom sheet UI: thumbnail preview, note input (200 char), submit/cancel - Upload service: multipart POST, Bearer auth, 10MB limit, offline cache, exponential backoff retry - Configurable server URL and API token via SharedPreferences - Minimal home screen with usage instructions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
95
lib/screens/home_screen.dart
Normal file
95
lib/screens/home_screen.dart
Normal file
@@ -0,0 +1,95 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../widgets/record_bottom_sheet.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
State<HomeScreen> createState() => _HomeScreenState();
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
static const _channel = MethodChannel('com.snapledger.snapledger/share');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_checkForSharedData();
|
||||
_channel.setMethodCallHandler((call) async {
|
||||
if (call.method == 'onShareReceived') {
|
||||
final args = Map<String, String>.from(call.arguments as Map);
|
||||
_handleSharedData(args);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _checkForSharedData() async {
|
||||
try {
|
||||
final data = await _channel.invokeMethod<Map>('getSharedData');
|
||||
if (data != null && data.containsKey('uri')) {
|
||||
_handleSharedData(Map<String, String>.from(data));
|
||||
}
|
||||
} on PlatformException {
|
||||
// No shared data
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleSharedData(Map<String, String> data) async {
|
||||
final filename = data['filename'] ?? 'upload.jpg';
|
||||
|
||||
// Resolve the content URI to a local file
|
||||
// Use MethodChannel to get the actual file path from content URI
|
||||
try {
|
||||
final filePath = await _channel.invokeMethod<String>('getFilePath', {'uri': data['uri']});
|
||||
if (filePath == null || !File(filePath).existsSync()) return;
|
||||
|
||||
if (!mounted) return;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (_) => RecordBottomSheet(
|
||||
imageFile: File(filePath),
|
||||
originalFilename: filename,
|
||||
),
|
||||
);
|
||||
} on PlatformException {
|
||||
// Could not resolve file
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('SnapLedger')),
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.receipt_long, size: 64, color: Colors.grey),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'如何使用',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'1. 在支付成功页面截图\n'
|
||||
'2. 点击分享按钮\n'
|
||||
'3. 选择 SnapLedger\n'
|
||||
'4. 添加备注(可选)并提交',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 16, color: Color(0xFF616161)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user