Flutter Android login vault: local DB (id+base64 content), email/Google auth, dashboard search/edit/swipe-delete, dark/system theme, minSdk 29

This commit is contained in:
cania
2026-08-12 11:41:20 +02:00
commit a8791a5b10
138 changed files with 6285 additions and 0 deletions

208
lib/dashboard_screen.dart Normal file
View File

@@ -0,0 +1,208 @@
import 'package:flutter/material.dart';
import 'package:login_vault_app/auth_service.dart';
import 'package:login_vault_app/database_helper.dart';
import 'package:login_vault_app/settings_screen.dart';
import 'package:login_vault_app/theme_provider.dart';
import 'package:login_vault_app/vault_entry_dialogs.dart';
/// Main dashboard: search ids, list entries, expand content, edit, swipe delete.
class DashboardScreen extends StatefulWidget {
final AuthService auth;
final ThemeProvider theme;
final String email;
const DashboardScreen(
{super.key,
required this.auth,
required this.theme,
required this.email});
@override
State<DashboardScreen> createState() => _DashboardScreenState();
}
class _DashboardScreenState extends State<DashboardScreen> {
final _db = DatabaseHelper();
final _searchCtrl = TextEditingController();
List<VaultEntry> _entries = [];
bool _loading = true;
@override
void initState() {
super.initState();
_searchCtrl.addListener(_onSearchChanged);
_refresh();
}
@override
void dispose() {
_searchCtrl.removeListener(_onSearchChanged);
_searchCtrl.dispose();
super.dispose();
}
void _onSearchChanged() => _refresh();
Future<void> _refresh() async {
setState(() => _loading = true);
final list = await _db.search(_searchCtrl.text);
if (mounted) {
setState(() {
_entries = list;
_loading = false;
});
}
}
Future<void> _deleteEntry(VaultEntry e) async {
final confirmed = await showConfirmDialog(
context,
title: 'Hapus entri?',
body: 'Yakin ingin menghapus "${e.id}"? Tindakan ini tidak dapat dibatalkan.',
);
if (confirmed == true) {
await _db.delete(e.id);
_refresh();
}
}
Future<void> _editEntry(VaultEntry e) async {
final result = await showEntryDialog(
context,
title: 'Edit konten',
id: e.id,
initialContent: e.content,
lockedId: true,
);
if (result != null) {
await _db.update(e.id, result.content);
_refresh();
}
}
Future<void> _addEntry() async {
final result = await showEntryDialog(
context,
title: 'Tambah entri baru',
id: '',
initialContent: '',
lockedId: false,
);
if (result != null && result.id.isNotEmpty) {
try {
await _db.insert(VaultEntry(id: result.id, content: result.content));
_refresh();
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Gagal: ID "${result.id}" sudah ada')),
);
}
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Login Vault'),
actions: [
IconButton(
icon: const Icon(Icons.settings),
tooltip: 'Pengaturan',
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => SettingsScreen(
auth: widget.auth, theme: widget.theme),
),
).then((_) => _refresh()),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: _addEntry,
tooltip: 'Tambah entri',
child: const Icon(Icons.add),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: TextField(
controller: _searchCtrl,
decoration: InputDecoration(
labelText: 'Cari ID...',
prefixIcon: const Icon(Icons.search),
border: const OutlineInputBorder(),
suffixIcon: _searchCtrl.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchCtrl.clear();
_refresh();
},
)
: null,
),
),
),
Expanded(
child: _loading
? const Center(child: CircularProgressIndicator())
: _entries.isEmpty
? const Center(child: Text('Tidak ada entri'))
: ListView.builder(
itemCount: _entries.length,
itemBuilder: (ctx, i) {
final e = _entries[i];
return Dismissible(
key: Key(e.id),
direction: DismissDirection.endToStart,
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding:
const EdgeInsets.symmetric(horizontal: 20),
child: const Icon(Icons.delete, color: Colors.white),
),
confirmDismiss: (_) async {
// Use our own dialog for nicer copy.
final c = await showConfirmDialog(
context,
title: 'Hapus entri?',
body:
'Yakin hapus "${e.id}"?',
);
if (c == true) await _deleteEntry(e);
return false; // we handle deletion ourselves
},
child: Card(
margin: const EdgeInsets.symmetric(
horizontal: 12, vertical: 6),
child: ExpansionTile(
title: Text(e.id,
style: const TextStyle(
fontWeight: FontWeight.bold)),
trailing: IconButton(
icon: const Icon(Icons.edit),
tooltip: 'Edit',
onPressed: () => _editEntry(e),
),
children: [
Padding(
padding: const EdgeInsets.all(16),
child: SelectableText(e.content),
),
],
),
),
);
},
),
),
],
),
);
}
}