Initial commit

This commit is contained in:
kihong.kim
2026-01-24 19:41:19 +09:00
commit 807df3d678
90 changed files with 6411 additions and 0 deletions

View File

@@ -0,0 +1,450 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../models/bible_verse.dart';
import '../../models/family_member.dart';
import '../../models/photo.dart';
import '../../services/bible_verse_service.dart';
import '../../services/family_service.dart';
import '../../services/photo_service.dart';
class AdminScreen extends StatefulWidget {
const AdminScreen({super.key});
@override
State<AdminScreen> createState() => _AdminScreenState();
}
class _AdminScreenState extends State<AdminScreen> {
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: const Text('Admin Settings'),
bottom: const TabBar(
tabs: [
Tab(text: 'Family Members'),
Tab(text: 'Photos'),
Tab(text: 'Bible Verses'),
],
),
),
body: const TabBarView(
children: [
FamilyManagerTab(),
PhotoManagerTab(),
BibleVerseManagerTab(),
],
),
),
);
}
}
class FamilyManagerTab extends StatefulWidget {
const FamilyManagerTab({super.key});
@override
State<FamilyManagerTab> createState() => _FamilyManagerTabState();
}
class _FamilyManagerTabState extends State<FamilyManagerTab> {
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddMemberDialog(context),
child: const Icon(Icons.add),
),
body: FutureBuilder<List<FamilyMember>>(
future: Provider.of<FamilyService>(context).fetchFamilyMembers(),
builder: (context, snapshot) {
if (!snapshot.hasData)
return const Center(child: CircularProgressIndicator());
final members = snapshot.data!;
return ListView.builder(
itemCount: members.length,
itemBuilder: (context, index) {
final member = members[index];
Color memberColor;
try {
memberColor = Color(
int.parse(member.color.replaceAll('#', '0xFF')),
);
} catch (_) {
memberColor = Colors.grey;
}
return ListTile(
leading: CircleAvatar(
backgroundColor: memberColor,
child: Text(
member.emoji,
style: const TextStyle(fontSize: 20),
),
),
title: Text(member.name),
subtitle: Text('Order: ${member.order}'),
trailing: IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () async {
await Provider.of<FamilyService>(
context,
listen: false,
).deleteFamilyMember(member.id);
setState(() {});
},
),
);
},
);
},
),
);
}
void _showAddMemberDialog(BuildContext context) {
final nameController = TextEditingController();
final emojiController = TextEditingController(text: '👤');
final colorController = TextEditingController(text: '0xFFFFD700');
final orderController = TextEditingController(text: '1');
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Add Family Member'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(labelText: 'Name'),
),
TextField(
controller: emojiController,
decoration: const InputDecoration(labelText: 'Emoji'),
),
TextField(
controller: colorController,
decoration: const InputDecoration(
labelText: 'Color (Hex 0xAARRGGBB)',
),
),
TextField(
controller: orderController,
decoration: const InputDecoration(labelText: 'Order'),
keyboardType: TextInputType.number,
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: () async {
if (nameController.text.isNotEmpty) {
await Provider.of<FamilyService>(
context,
listen: false,
).createFamilyMember(
FamilyMember(
id: '',
name: nameController.text,
emoji: emojiController.text,
color: colorController.text.replaceFirst(
'0xFF',
'#',
), // Simple conversion
order: int.tryParse(orderController.text) ?? 1,
),
);
if (mounted) {
Navigator.pop(context);
setState(() {});
}
}
},
child: const Text('Add'),
),
],
),
);
}
}
class PhotoManagerTab extends StatefulWidget {
const PhotoManagerTab({super.key});
@override
State<PhotoManagerTab> createState() => _PhotoManagerTabState();
}
class _PhotoManagerTabState extends State<PhotoManagerTab> {
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddPhotoDialog(context),
child: const Icon(Icons.add_a_photo),
),
body: FutureBuilder<List<Photo>>(
future: Provider.of<PhotoService>(context).fetchPhotos(),
builder: (context, snapshot) {
if (!snapshot.hasData)
return const Center(child: CircularProgressIndicator());
final photos = snapshot.data!;
return GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: photos.length,
itemBuilder: (context, index) {
final photo = photos[index];
return GridTile(
footer: GridTileBar(
backgroundColor: Colors.black54,
title: Text(photo.caption),
trailing: IconButton(
icon: const Icon(Icons.delete, color: Colors.white),
onPressed: () async {
await Provider.of<PhotoService>(
context,
listen: false,
).deletePhoto(photo.id);
setState(() {});
},
),
),
child: Image.network(
photo.url,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) =>
const Center(child: Icon(Icons.broken_image)),
),
);
},
);
},
),
);
}
void _showAddPhotoDialog(BuildContext context) {
final urlController = TextEditingController();
final captionController = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Add Photo URL'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: urlController,
decoration: const InputDecoration(labelText: 'Image URL'),
),
TextField(
controller: captionController,
decoration: const InputDecoration(labelText: 'Caption'),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: () async {
if (urlController.text.isNotEmpty) {
await Provider.of<PhotoService>(
context,
listen: false,
).createPhoto(
Photo(
id: '',
url: urlController.text,
caption: captionController.text,
active: true,
),
);
if (mounted) {
Navigator.pop(context);
setState(() {});
}
}
},
child: const Text('Add'),
),
],
),
);
}
}
class BibleVerseManagerTab extends StatefulWidget {
const BibleVerseManagerTab({super.key});
@override
State<BibleVerseManagerTab> createState() => _BibleVerseManagerTabState();
}
class _BibleVerseManagerTabState extends State<BibleVerseManagerTab> {
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddVerseDialog(context),
child: const Icon(Icons.menu_book),
),
body: FutureBuilder<List<BibleVerse>>(
future: Provider.of<BibleVerseService>(context).fetchVerses(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final verses = snapshot.data!;
if (verses.isEmpty) {
return const Center(
child: Text(
'No verses added yet',
style: TextStyle(color: Colors.grey),
),
);
}
return ListView.builder(
itemCount: verses.length,
itemBuilder: (context, index) {
final verse = verses[index];
return ListTile(
title: Text(verse.reference),
subtitle: Text(
verse.text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (verse.date != null && verse.date!.isNotEmpty)
Text(
verse.date!,
style:
const TextStyle(fontSize: 12, color: Colors.grey),
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () async {
await Provider.of<BibleVerseService>(
context,
listen: false,
).deleteVerse(verse.id);
setState(() {});
},
),
],
),
);
},
);
},
),
);
}
void _showAddVerseDialog(BuildContext context) {
final textController = TextEditingController();
final referenceController = TextEditingController();
final dateController = TextEditingController();
bool isActive = true;
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
title: const Text('Add Bible Verse'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: referenceController,
decoration: const InputDecoration(
labelText: 'Reference (e.g., Psalms 23:1)',
),
),
const SizedBox(height: 8),
TextField(
controller: textController,
decoration: const InputDecoration(
labelText: 'Verse Text (Korean)',
),
maxLines: 3,
),
const SizedBox(height: 8),
TextField(
controller: dateController,
decoration: const InputDecoration(
labelText: 'Date (YYYY-MM-DD) - Optional',
hintText: '2024-01-01',
),
),
const SizedBox(height: 8),
SwitchListTile(
title: const Text('Active'),
value: isActive,
onChanged: (value) {
setDialogState(() {
isActive = value;
});
},
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: () async {
if (textController.text.isNotEmpty &&
referenceController.text.isNotEmpty) {
await Provider.of<BibleVerseService>(
context,
listen: false,
).createVerse(
BibleVerse(
id: '',
text: textController.text,
reference: referenceController.text,
date: dateController.text.isEmpty
? null
: dateController.text,
active: isActive,
),
);
if (mounted) {
Navigator.pop(context);
setState(() {});
}
}
},
child: const Text('Add'),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,431 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import '../../models/todo_item.dart';
import '../../models/schedule_item.dart';
import '../../models/announcement.dart';
import '../../models/family_member.dart';
import '../../services/todo_service.dart';
import '../../services/schedule_service.dart';
import '../../services/announcement_service.dart';
import '../../services/family_service.dart';
class MobileHomeScreen extends StatefulWidget {
const MobileHomeScreen({super.key});
@override
State<MobileHomeScreen> createState() => _MobileHomeScreenState();
}
class _MobileHomeScreenState extends State<MobileHomeScreen> {
int _currentIndex = 0;
final List<Widget> _screens = [
const MobileTodoScreen(),
const MobileScheduleScreen(),
const MobileAnnouncementScreen(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Bini Family Manager'),
actions: [
IconButton(
icon: const Icon(Icons.settings),
onPressed: () {
Navigator.pushNamed(context, '/admin');
},
),
],
),
body: _screens[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
items: const [
BottomNavigationBarItem(icon: Icon(Icons.check_box), label: 'Todos'),
BottomNavigationBarItem(
icon: Icon(Icons.calendar_today),
label: 'Schedule',
),
BottomNavigationBarItem(icon: Icon(Icons.campaign), label: 'Notices'),
],
),
);
}
}
class MobileTodoScreen extends StatefulWidget {
const MobileTodoScreen({super.key});
@override
State<MobileTodoScreen> createState() => _MobileTodoScreenState();
}
class _MobileTodoScreenState extends State<MobileTodoScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddTodoDialog(context),
child: const Icon(Icons.add),
),
body: FutureBuilder<List<TodoItem>>(
future: Provider.of<TodoService>(
context,
).fetchTodos(), // Fetch all or today? Let's fetch all for manager
builder: (context, snapshot) {
if (!snapshot.hasData)
return const Center(child: CircularProgressIndicator());
final todos = snapshot.data!;
return ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return ListTile(
title: Text(todo.title),
subtitle: Text(
todo.dueDate != null
? DateFormat('MM/dd').format(todo.dueDate!)
: 'No date',
),
trailing: Checkbox(
value: todo.completed,
onChanged: (val) async {
await Provider.of<TodoService>(
context,
listen: false,
).updateTodo(
TodoItem(
id: todo.id,
familyMemberId: todo.familyMemberId,
title: todo.title,
completed: val ?? false,
dueDate: todo.dueDate,
),
);
setState(() {});
},
),
onLongPress: () async {
await Provider.of<TodoService>(
context,
listen: false,
).deleteTodo(todo.id);
setState(() {});
},
);
},
);
},
),
);
}
void _showAddTodoDialog(BuildContext context) async {
final titleController = TextEditingController();
final familyMembers = await Provider.of<FamilyService>(
context,
listen: false,
).fetchFamilyMembers();
String? selectedMemberId =
familyMembers.isNotEmpty ? familyMembers.first.id : null;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Add Todo'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: titleController,
decoration: const InputDecoration(labelText: 'Task'),
),
DropdownButtonFormField<String>(
value: selectedMemberId,
items: familyMembers
.map(
(m) => DropdownMenuItem(value: m.id, child: Text(m.name)),
)
.toList(),
onChanged: (val) => selectedMemberId = val,
decoration: const InputDecoration(labelText: 'Assign to'),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: () async {
if (selectedMemberId != null && titleController.text.isNotEmpty) {
await Provider.of<TodoService>(
context,
listen: false,
).createTodo(
TodoItem(
id: '',
familyMemberId: selectedMemberId!,
title: titleController.text,
completed: false,
dueDate: DateTime.now(),
),
);
if (mounted) {
Navigator.pop(context);
setState(() {});
}
}
},
child: const Text('Add'),
),
],
),
);
}
}
class MobileScheduleScreen extends StatefulWidget {
const MobileScheduleScreen({super.key});
@override
State<MobileScheduleScreen> createState() => _MobileScheduleScreenState();
}
class _MobileScheduleScreenState extends State<MobileScheduleScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddScheduleDialog(context),
child: const Icon(Icons.add),
),
body: FutureBuilder<List<ScheduleItem>>(
future: Provider.of<ScheduleService>(context).fetchSchedules(),
builder: (context, snapshot) {
if (!snapshot.hasData)
return const Center(child: CircularProgressIndicator());
final schedules = snapshot.data!;
return ListView.builder(
itemCount: schedules.length,
itemBuilder: (context, index) {
final item = schedules[index];
return ListTile(
title: Text(item.title),
subtitle: Text(
'${DateFormat('MM/dd HH:mm').format(item.startDate)} - ${item.description}',
),
onLongPress: () async {
await Provider.of<ScheduleService>(
context,
listen: false,
).deleteSchedule(item.id);
setState(() {});
},
);
},
);
},
),
);
}
void _showAddScheduleDialog(BuildContext context) async {
final titleController = TextEditingController();
final descController = TextEditingController();
final familyMembers = await Provider.of<FamilyService>(
context,
listen: false,
).fetchFamilyMembers();
String? selectedMemberId =
familyMembers.isNotEmpty ? familyMembers.first.id : null;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Add Schedule'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: titleController,
decoration: const InputDecoration(labelText: 'Title'),
),
TextField(
controller: descController,
decoration: const InputDecoration(labelText: 'Description'),
),
DropdownButtonFormField<String>(
value: selectedMemberId,
items: familyMembers
.map(
(m) => DropdownMenuItem(value: m.id, child: Text(m.name)),
)
.toList(),
onChanged: (val) => selectedMemberId = val,
decoration: const InputDecoration(labelText: 'For whom?'),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: () async {
if (selectedMemberId != null && titleController.text.isNotEmpty) {
await Provider.of<ScheduleService>(
context,
listen: false,
).createSchedule(
ScheduleItem(
id: '',
title: titleController.text,
description: descController.text,
startDate: DateTime.now(),
endDate: DateTime.now().add(const Duration(hours: 1)),
familyMemberId: selectedMemberId!,
isAllDay: false,
),
);
if (mounted) {
Navigator.pop(context);
setState(() {});
}
}
},
child: const Text('Add'),
),
],
),
);
}
}
class MobileAnnouncementScreen extends StatefulWidget {
const MobileAnnouncementScreen({super.key});
@override
State<MobileAnnouncementScreen> createState() =>
_MobileAnnouncementScreenState();
}
class _MobileAnnouncementScreenState extends State<MobileAnnouncementScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddAnnouncementDialog(context),
child: const Icon(Icons.add),
),
body: FutureBuilder<List<Announcement>>(
future: Provider.of<AnnouncementService>(context).fetchAnnouncements(),
builder: (context, snapshot) {
if (!snapshot.hasData)
return const Center(child: CircularProgressIndicator());
final items = snapshot.data!;
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return ListTile(
title: Text(item.title),
subtitle: Text(item.content),
trailing: Switch(
value: item.active,
onChanged: (val) async {
await Provider.of<AnnouncementService>(
context,
listen: false,
).updateAnnouncement(
Announcement(
id: item.id,
title: item.title,
content: item.content,
priority: item.priority,
active: val,
),
);
setState(() {});
},
),
onLongPress: () async {
await Provider.of<AnnouncementService>(
context,
listen: false,
).deleteAnnouncement(item.id);
setState(() {});
},
);
},
);
},
),
);
}
void _showAddAnnouncementDialog(BuildContext context) {
final titleController = TextEditingController();
final contentController = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Add Announcement'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: titleController,
decoration: const InputDecoration(labelText: 'Title'),
),
TextField(
controller: contentController,
decoration: const InputDecoration(labelText: 'Content'),
maxLines: 3,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: () async {
if (titleController.text.isNotEmpty) {
await Provider.of<AnnouncementService>(
context,
listen: false,
).createAnnouncement(
Announcement(
id: '',
title: titleController.text,
content: contentController.text,
priority: 1,
active: true,
),
);
if (mounted) {
Navigator.pop(context);
setState(() {});
}
}
},
child: const Text('Add'),
),
],
),
);
}
}

View File

@@ -0,0 +1,133 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../widgets/digital_clock_widget.dart';
import '../../widgets/weather_widget.dart';
import '../../widgets/calendar_widget.dart';
import '../../widgets/schedule_list_widget.dart';
import '../../widgets/announcement_widget.dart';
import '../../widgets/photo_slideshow_widget.dart';
import '../../widgets/todo_list_widget.dart';
import '../../widgets/bible_verse_widget.dart';
class TvDashboardScreen extends StatefulWidget {
const TvDashboardScreen({super.key});
@override
State<TvDashboardScreen> createState() => _TvDashboardScreenState();
}
class _TvDashboardScreenState extends State<TvDashboardScreen> {
// Timer for periodic refresh (every 5 minutes for data, 1 second for clock)
Timer? _dataRefreshTimer;
@override
void initState() {
super.initState();
// Initial data fetch could be triggered here or within widgets
_startDataRefresh();
}
void _startDataRefresh() {
_dataRefreshTimer = Timer.periodic(const Duration(minutes: 5), (timer) {
// Trigger refreshes if needed, or let widgets handle their own polling
// For simplicity, we assume widgets or providers handle their data
setState(() {}); // Rebuild to refresh UI state if needed
});
}
@override
void dispose() {
_dataRefreshTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
// 1920x1080 reference
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(32.0), // Outer margin safe zone
child: Column(
children: [
// Header: Time and Weather
SizedBox(
height: 100,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [const DigitalClockWidget(), const WeatherWidget()],
),
),
const SizedBox(height: 24),
// Main Content Grid
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Left Column: Calendar, Schedule, Announcement
Expanded(
flex: 3,
child: Column(
children: [
const Expanded(flex: 4, child: CalendarWidget()),
const SizedBox(height: 16),
const Expanded(flex: 4, child: ScheduleListWidget()),
const SizedBox(height: 16),
const Expanded(flex: 2, child: AnnouncementWidget()),
],
),
),
const SizedBox(width: 24),
// Center Column: Photo Slideshow
Expanded(
flex: 4,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.5),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
clipBehavior: Clip.antiAlias,
child: const PhotoSlideshowWidget(),
),
),
const SizedBox(width: 24),
// Right Column: Todos, Bible Verse
Expanded(
flex: 3,
child: Column(
children: [
const Expanded(flex: 6, child: TodoListWidget()),
const SizedBox(height: 16),
const Expanded(flex: 3, child: BibleVerseWidget()),
],
),
),
],
),
),
// Hidden trigger for admin/mobile view (e.g. long press corner)
GestureDetector(
onLongPress: () {
Navigator.of(context).pushNamed('/admin');
},
child: Container(
width: 50,
height: 50,
color: Colors.transparent,
),
),
],
),
),
),
);
}
}