1138 lines
37 KiB
Dart
1138 lines
37 KiB
Dart
part of 'main.dart';
|
|
|
|
class PersonsPage extends StatelessWidget {
|
|
const PersonsPage({
|
|
super.key,
|
|
required this.people,
|
|
required this.events,
|
|
required this.onAdd,
|
|
required this.onEdit,
|
|
required this.onDelete,
|
|
});
|
|
|
|
final List<Person> people;
|
|
final List<CalendarEvent> events;
|
|
final VoidCallback onAdd;
|
|
final ValueChanged<Person> onEdit;
|
|
final ValueChanged<Person> onDelete;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return PageFrame(
|
|
eyebrow: '${people.length} csal\u00e1dtag',
|
|
title: 'Csal\u00e1dtagok',
|
|
action: FilledButton.icon(
|
|
onPressed: onAdd,
|
|
icon: const Icon(Icons.add),
|
|
label: const Text('\u00daj csal\u00e1dtag'),
|
|
),
|
|
child: people.isEmpty
|
|
? const EmptyState(
|
|
icon: Icons.group_add_outlined,
|
|
title: 'M\u00e9g nincs csal\u00e1dtag',
|
|
subtitle:
|
|
'Vegy\u00e9l fel csal\u00e1dtagokat, gyerekeket vagy b\u00e1rkit.',
|
|
)
|
|
: ListView.separated(
|
|
padding: const EdgeInsets.only(bottom: 24),
|
|
itemCount: people.length,
|
|
separatorBuilder: (_, _) => const SizedBox(height: 10),
|
|
itemBuilder: (_, i) {
|
|
final person = people[i];
|
|
final personEvents =
|
|
events
|
|
.where((event) => event.personId == person.id)
|
|
.toList()
|
|
..sort((a, b) => a.date.compareTo(b.date));
|
|
final nextEvent = personEvents.isEmpty
|
|
? null
|
|
: personEvents.first;
|
|
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(18),
|
|
child: Row(
|
|
children: [
|
|
FamilyAvatar(person: person),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
person.name,
|
|
style: const TextStyle(
|
|
color: ink,
|
|
fontWeight: FontWeight.w800,
|
|
fontSize: 17,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
nextEvent == null
|
|
? 'Nincs hozz\u00e1rendelt esem\u00e9ny'
|
|
: 'K\u00f6vetkez\u0151: ${nextEvent.title}, ${formatDate(nextEvent.date)} ${nextEvent.start}',
|
|
style: const TextStyle(color: Colors.black54),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Text(
|
|
'${personEvents.length} esem\u00e9ny',
|
|
style: const TextStyle(
|
|
color: sage,
|
|
fontWeight: FontWeight.w800,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
PopupMenuButton<String>(
|
|
onSelected: (value) {
|
|
if (value == 'edit') onEdit(person);
|
|
if (value == 'delete') onDelete(person);
|
|
},
|
|
itemBuilder: (_) => const [
|
|
PopupMenuItem(
|
|
value: 'edit',
|
|
child: Text('\u00c1tnevez\u00e9s / avatar'),
|
|
),
|
|
PopupMenuItem(
|
|
value: 'delete',
|
|
child: Text('T\u00f6rl\u00e9s'),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class StylePage extends StatelessWidget {
|
|
const StylePage({
|
|
super.key,
|
|
required this.themeId,
|
|
required this.onThemeChanged,
|
|
});
|
|
|
|
final String themeId;
|
|
final ValueChanged<String> onThemeChanged;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return PageFrame(
|
|
eyebrow: 'T\u00e9ma \u00e9s st\u00edlusok',
|
|
title: 'Megjelen\u00e9s',
|
|
child: ListView(
|
|
padding: const EdgeInsets.only(bottom: 24),
|
|
children: [
|
|
const Text(
|
|
'V\u00e1lassz egy alkalmaz\u00e1st\u00e9m\u00e1t. A v\u00e1laszt\u00e1s JSON-be ment\u0151dik, \u00e9s \u00fajraind\u00edt\u00e1s ut\u00e1n is megmarad.',
|
|
style: TextStyle(color: Colors.black54, fontSize: 16),
|
|
),
|
|
const SizedBox(height: 18),
|
|
...appThemes.map((theme) {
|
|
final selected = theme.id == themeId;
|
|
return Card(
|
|
child: ListTile(
|
|
contentPadding: const EdgeInsets.all(16),
|
|
leading: CircleAvatar(backgroundColor: theme.seed),
|
|
title: Text(
|
|
theme.name,
|
|
style: const TextStyle(
|
|
color: ink,
|
|
fontWeight: FontWeight.w800,
|
|
),
|
|
),
|
|
subtitle: Text(theme.description),
|
|
trailing: selected
|
|
? const Icon(Icons.check_circle, color: sage)
|
|
: const Icon(Icons.circle_outlined, color: Colors.black26),
|
|
onTap: () => onThemeChanged(theme.id),
|
|
),
|
|
);
|
|
}),
|
|
const SizedBox(height: 18),
|
|
const Text(
|
|
'Csal\u00e1dtag sz\u00ednek \u00e9s avatar k\u00e9szlet',
|
|
style: TextStyle(
|
|
color: ink,
|
|
fontWeight: FontWeight.w800,
|
|
fontSize: 18,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: avatarPresets
|
|
.map(
|
|
(preset) => Chip(
|
|
avatar: Text(preset.symbol),
|
|
label: Text(preset.label),
|
|
),
|
|
)
|
|
.toList(),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ShoppingPage extends StatelessWidget {
|
|
const ShoppingPage({
|
|
super.key,
|
|
required this.items,
|
|
required this.onChanged,
|
|
required this.onAdd,
|
|
});
|
|
|
|
final List<ShoppingItem> items;
|
|
final VoidCallback onChanged;
|
|
final VoidCallback onAdd;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final remaining = items.where((e) => !e.done).length;
|
|
return PageFrame(
|
|
eyebrow: '$remaining t\u00e9tel van h\u00e1tra',
|
|
title: 'Bev\u00e1s\u00e1rl\u00f3lista',
|
|
action: IconButton.filled(onPressed: onAdd, icon: const Icon(Icons.add)),
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(18),
|
|
decoration: BoxDecoration(
|
|
color: mint,
|
|
borderRadius: BorderRadius.circular(18),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
const Icon(Icons.auto_awesome_outlined, color: sage),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Text(
|
|
items.isEmpty
|
|
? 'A list\u00e1d \u00fcres.'
|
|
: '${items.length - remaining} t\u00e9tel m\u00e1r a kos\u00e1rban van.',
|
|
style: const TextStyle(color: ink),
|
|
),
|
|
),
|
|
Text(
|
|
'${items.length - remaining}/${items.length}',
|
|
style: const TextStyle(
|
|
color: sage,
|
|
fontWeight: FontWeight.w800,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Expanded(
|
|
child: ListView.separated(
|
|
padding: const EdgeInsets.only(bottom: 24),
|
|
itemCount: items.length,
|
|
separatorBuilder: (_, _) => const SizedBox(height: 8),
|
|
itemBuilder: (_, i) {
|
|
final item = items[i];
|
|
return Card(
|
|
child: CheckboxListTile(
|
|
value: item.done,
|
|
activeColor: sage,
|
|
onChanged: (value) {
|
|
item.done = value ?? false;
|
|
onChanged();
|
|
},
|
|
title: Text(
|
|
item.name,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w700,
|
|
decoration: item.done
|
|
? TextDecoration.lineThrough
|
|
: null,
|
|
color: item.done ? Colors.black38 : ink,
|
|
),
|
|
),
|
|
subtitle: Text('${item.amount} \u00b7 ${item.category}'),
|
|
secondary: const Icon(
|
|
Icons.drag_indicator,
|
|
color: Colors.black26,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class StockPage extends StatefulWidget {
|
|
const StockPage({
|
|
super.key,
|
|
required this.items,
|
|
required this.onChanged,
|
|
required this.onAdd,
|
|
required this.onEdit,
|
|
required this.jarvisSettings,
|
|
});
|
|
|
|
final List<StockItem> items;
|
|
final VoidCallback onChanged;
|
|
final VoidCallback onAdd;
|
|
final ValueChanged<StockItem> onEdit;
|
|
final JarvisSettings jarvisSettings;
|
|
|
|
@override
|
|
State<StockPage> createState() => _StockPageState();
|
|
}
|
|
|
|
class _StockPageState extends State<StockPage> {
|
|
String location = 'H\u0171t\u0151';
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final filtered = widget.items.where((e) => e.location == location).toList();
|
|
return PageFrame(
|
|
eyebrow: '${widget.items.length} term\u00e9k nyilv\u00e1ntartva',
|
|
title: 'Otthoni k\u00e9szlet',
|
|
action: IconButton.filled(
|
|
onPressed: widget.onAdd,
|
|
icon: const Icon(Icons.add),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
SegmentedButton<String>(
|
|
segments: const [
|
|
ButtonSegment(
|
|
value: 'H\u0171t\u0151',
|
|
label: Text('H\u0171t\u0151szekr\u00e9ny'),
|
|
icon: Icon(Icons.kitchen),
|
|
),
|
|
ButtonSegment(
|
|
value: 'Kamra',
|
|
label: Text('Sz\u00e1raz\u00e1ru'),
|
|
icon: Icon(Icons.inventory_2_outlined),
|
|
),
|
|
],
|
|
selected: {location},
|
|
onSelectionChanged: (s) => setState(() => location = s.first),
|
|
),
|
|
const SizedBox(height: 18),
|
|
Expanded(
|
|
child: ListView.separated(
|
|
padding: const EdgeInsets.only(bottom: 24),
|
|
itemCount: filtered.length,
|
|
separatorBuilder: (_, _) => const SizedBox(height: 9),
|
|
itemBuilder: (_, i) {
|
|
final item = filtered[i];
|
|
final warning = item.expiry == 'j\u00fal. 22.';
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(15),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 44,
|
|
height: 44,
|
|
decoration: BoxDecoration(
|
|
color: warning ? const Color(0xFFFFEBDD) : mint,
|
|
borderRadius: BorderRadius.circular(13),
|
|
),
|
|
child: Icon(
|
|
location == 'H\u0171t\u0151'
|
|
? Icons.egg_alt_outlined
|
|
: Icons.grain,
|
|
color: warning ? orange : sage,
|
|
),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
item.name,
|
|
style: const TextStyle(
|
|
color: ink,
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
const SizedBox(height: 3),
|
|
Text(
|
|
'Lej\u00e1rat: ${item.expiry}',
|
|
style: TextStyle(
|
|
color: warning ? orange : Colors.black45,
|
|
fontWeight: warning
|
|
? FontWeight.w700
|
|
: FontWeight.normal,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
IconButton(
|
|
onPressed: item.amount > 0
|
|
? () {
|
|
item.amount--;
|
|
widget.onChanged();
|
|
}
|
|
: null,
|
|
icon: const Icon(Icons.remove_circle_outline),
|
|
),
|
|
Text(
|
|
'${item.amount} ${item.unit}',
|
|
style: const TextStyle(
|
|
color: ink,
|
|
fontWeight: FontWeight.w800,
|
|
),
|
|
),
|
|
IconButton(
|
|
onPressed: () {
|
|
item.amount++;
|
|
widget.onChanged();
|
|
},
|
|
icon: const Icon(Icons.add_circle_outline),
|
|
),
|
|
IconButton(
|
|
tooltip: 'K\u00e9szlet szerkeszt\u00e9se',
|
|
onPressed: () => widget.onEdit(item),
|
|
icon: const Icon(Icons.edit_outlined),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class AccountsPage extends StatefulWidget {
|
|
const AccountsPage({
|
|
super.key,
|
|
required this.connections,
|
|
required this.cachedEvents,
|
|
required this.people,
|
|
required this.onChanged,
|
|
required this.onSyncedEventsChanged,
|
|
});
|
|
|
|
final List<GoogleCalendarConnection> connections;
|
|
final List<CalendarEvent> cachedEvents;
|
|
final List<Person> people;
|
|
final ValueChanged<List<GoogleCalendarConnection>> onChanged;
|
|
final ValueChanged<List<CalendarEvent>> onSyncedEventsChanged;
|
|
|
|
@override
|
|
State<AccountsPage> createState() => _AccountsPageState();
|
|
}
|
|
|
|
class _AccountsPageState extends State<AccountsPage> {
|
|
final calendarPlugin = device_calendar.DeviceCalendarPlugin();
|
|
List<device_calendar.Calendar> deviceCalendars = [];
|
|
bool loadingCalendars = false;
|
|
bool syncing = false;
|
|
String? statusMessage;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadDeviceCalendars();
|
|
}
|
|
|
|
GoogleCalendarConnection? get connection =>
|
|
widget.connections.isEmpty ? null : widget.connections.first;
|
|
|
|
Future<void> _loadDeviceCalendars() async {
|
|
setState(() {
|
|
loadingCalendars = true;
|
|
statusMessage = null;
|
|
});
|
|
|
|
try {
|
|
final permissions = await calendarPlugin.requestPermissions();
|
|
if (permissions.isSuccess != true || permissions.data != true) {
|
|
setState(() {
|
|
loadingCalendars = false;
|
|
statusMessage =
|
|
'Napt\u00e1r olvas\u00e1si enged\u00e9ly n\u00e9lk\u00fcl nem lehet szinkroniz\u00e1lni.';
|
|
});
|
|
return;
|
|
}
|
|
|
|
final result = await calendarPlugin.retrieveCalendars();
|
|
final calendars =
|
|
(result.data ?? const <device_calendar.Calendar>[])
|
|
.where((calendar) => calendar.id != null)
|
|
.toList()
|
|
..sort((a, b) => (a.name ?? '').compareTo(b.name ?? ''));
|
|
|
|
setState(() {
|
|
deviceCalendars = calendars;
|
|
loadingCalendars = false;
|
|
if (calendars.isEmpty) {
|
|
statusMessage =
|
|
'Nem tal\u00e1ltam lok\u00e1lis napt\u00e1rat az eszk\u00f6z\u00f6n. Androidon ellen\u0151rizd: Be\u00e1ll\u00edt\u00e1sok > Fi\u00f3kok > Google > Fi\u00f3kszinkroniz\u00e1l\u00e1s > Napt\u00e1r legyen bekapcsolva.';
|
|
} else {
|
|
final accountTypes = calendars
|
|
.map((calendar) => calendar.accountType)
|
|
.whereType<String>()
|
|
.where((type) => type.trim().isNotEmpty)
|
|
.toSet()
|
|
.toList()
|
|
..sort();
|
|
statusMessage =
|
|
'${calendars.length} napt\u00e1rat l\u00e1tok az eszk\u00f6z\u00f6n.'
|
|
'${accountTypes.isEmpty ? '' : ' Account t\u00edpusok: ${accountTypes.join(', ')}.'}';
|
|
}
|
|
});
|
|
} catch (error) {
|
|
setState(() {
|
|
loadingCalendars = false;
|
|
statusMessage =
|
|
'Nem siker\u00fclt lek\u00e9rni az eszk\u00f6z napt\u00e1rait: $error';
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _syncSelectedCalendars() async {
|
|
final current = connection;
|
|
if (current == null || current.enabledCalendarIds.isEmpty) {
|
|
setState(
|
|
() =>
|
|
statusMessage = 'V\u00e1lassz ki legal\u00e1bb egy napt\u00e1rat.',
|
|
);
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
syncing = true;
|
|
statusMessage =
|
|
'Esem\u00e9nyek beolvas\u00e1sa a lok\u00e1lis napt\u00e1rt\u00e1rb\u00f3l\u2026';
|
|
});
|
|
|
|
try {
|
|
final selectedCalendars = deviceCalendars
|
|
.where((calendar) => current.enabledCalendarIds.contains(calendar.id))
|
|
.toList();
|
|
final synced = <CalendarEvent>[];
|
|
final syncDetails = <String>[];
|
|
final now = DateTime.now();
|
|
final start = DateTime(
|
|
now.year,
|
|
now.month,
|
|
now.day,
|
|
).subtract(const Duration(days: 30));
|
|
final end = start.add(const Duration(days: 180));
|
|
|
|
for (final calendar in selectedCalendars) {
|
|
final result = await calendarPlugin.retrieveEvents(
|
|
calendar.id,
|
|
device_calendar.RetrieveEventsParams(startDate: start, endDate: end),
|
|
);
|
|
final events = result.data ?? const <device_calendar.Event>[];
|
|
syncDetails.add('${calendar.name ?? 'Napt\u00e1r'}: ${events.length}');
|
|
for (final event in events) {
|
|
synced.add(
|
|
CalendarEvent.fromDeviceEvent(
|
|
event,
|
|
calendar,
|
|
personId: current.calendarPersonIds[calendar.id],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
synced.sort((a, b) {
|
|
final dateCompare = a.date.compareTo(b.date);
|
|
return dateCompare != 0 ? dateCompare : a.start.compareTo(b.start);
|
|
});
|
|
|
|
widget.onSyncedEventsChanged(synced);
|
|
widget.onChanged([current.copyWith(lastSync: DateTime.now())]);
|
|
setState(() {
|
|
syncing = false;
|
|
statusMessage =
|
|
'${synced.length} esem\u00e9ny szinkroniz\u00e1lva \u00e9s JSON cache-be mentve.'
|
|
'${syncDetails.isEmpty ? '' : ' R\u00e9szletek: ${syncDetails.join('; ')}.'}';
|
|
});
|
|
} catch (error) {
|
|
setState(() {
|
|
syncing = false;
|
|
statusMessage = 'A szinkroniz\u00e1l\u00e1s nem siker\u00fclt: $error';
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final current = connection;
|
|
final hasConnection = current != null;
|
|
final cachedUncategorized = widget.cachedEvents
|
|
.where((event) => event.isUncategorized)
|
|
.length;
|
|
|
|
return PageFrame(
|
|
eyebrow: 'Lok\u00e1lis napt\u00e1r szinkron',
|
|
title: 'Eszk\u00f6z napt\u00e1rak',
|
|
action: FilledButton.icon(
|
|
onPressed: () => _connectGoogle(context),
|
|
icon: const Icon(Icons.add_link),
|
|
label: Text(
|
|
hasConnection
|
|
? 'Fi\u00f3k c\u00edmk\u00e9je'
|
|
: 'Fi\u00f3k c\u00edmke',
|
|
),
|
|
),
|
|
child: ListView(
|
|
padding: const EdgeInsets.only(bottom: 24),
|
|
children: [
|
|
GoogleSyncHero(
|
|
accountEmail: current?.accountEmail,
|
|
enabledCount: current?.enabledCalendarIds.length ?? 0,
|
|
cachedCount: widget.cachedEvents.length,
|
|
uncategorizedCount: cachedUncategorized,
|
|
),
|
|
const SizedBox(height: 22),
|
|
if (!hasConnection)
|
|
const EmptyGoogleConnectionCard()
|
|
else ...[
|
|
const Text(
|
|
'Eszk\u00f6z\u00f6n el\u00e9rhet\u0151 napt\u00e1rak',
|
|
style: TextStyle(
|
|
color: ink,
|
|
fontWeight: FontWeight.w800,
|
|
fontSize: 18,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
if (loadingCalendars)
|
|
const LinearProgressIndicator()
|
|
else
|
|
...deviceCalendars.map(
|
|
(calendar) => DeviceCalendarToggleCard(
|
|
calendar: calendar,
|
|
enabled: current.enabledCalendarIds.contains(calendar.id),
|
|
people: widget.people,
|
|
selectedPersonId: current.calendarPersonIds[calendar.id],
|
|
onChanged: (enabled) {
|
|
final nextIds = [...current.enabledCalendarIds];
|
|
final nextPersonIds = {...current.calendarPersonIds};
|
|
if (enabled) {
|
|
if (!nextIds.contains(calendar.id)) {
|
|
nextIds.add(calendar.id!);
|
|
}
|
|
} else {
|
|
nextIds.remove(calendar.id);
|
|
nextPersonIds.remove(calendar.id);
|
|
}
|
|
widget.onChanged([
|
|
current.copyWith(
|
|
enabledCalendarIds: nextIds,
|
|
calendarPersonIds: nextPersonIds,
|
|
lastSync: current.lastSync,
|
|
),
|
|
]);
|
|
},
|
|
onPersonChanged: (personId) {
|
|
if (calendar.id == null) return;
|
|
final nextPersonIds = {...current.calendarPersonIds};
|
|
if (personId == null) {
|
|
nextPersonIds.remove(calendar.id);
|
|
} else {
|
|
nextPersonIds[calendar.id!] = personId;
|
|
}
|
|
widget.onChanged([
|
|
current.copyWith(
|
|
calendarPersonIds: nextPersonIds,
|
|
lastSync: current.lastSync,
|
|
),
|
|
]);
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
FilledButton.icon(
|
|
onPressed: syncing ? null : _syncSelectedCalendars,
|
|
icon: syncing
|
|
? const SizedBox(
|
|
width: 18,
|
|
height: 18,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
)
|
|
: const Icon(Icons.sync),
|
|
label: Text(
|
|
syncing
|
|
? 'Szinkroniz\u00e1l\u00e1s\u2026'
|
|
: 'Szinkroniz\u00e1l\u00e1s most',
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
OutlinedButton.icon(
|
|
onPressed: _loadDeviceCalendars,
|
|
icon: const Icon(Icons.refresh),
|
|
label: const Text('Napt\u00e1rlista friss\u00edt\u00e9se'),
|
|
),
|
|
const SizedBox(height: 8),
|
|
OutlinedButton.icon(
|
|
onPressed: () {
|
|
widget.onChanged([]);
|
|
widget.onSyncedEventsChanged([]);
|
|
},
|
|
icon: const Icon(Icons.link_off),
|
|
label: const Text('Napt\u00e1rkapcsolat lev\u00e1laszt\u00e1sa'),
|
|
),
|
|
],
|
|
if (statusMessage != null) ...[
|
|
const SizedBox(height: 14),
|
|
Text(statusMessage!, style: const TextStyle(color: Colors.black54)),
|
|
],
|
|
const SizedBox(height: 24),
|
|
Container(
|
|
padding: const EdgeInsets.all(18),
|
|
decoration: BoxDecoration(
|
|
color: mint,
|
|
borderRadius: BorderRadius.circular(18),
|
|
),
|
|
child: const Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Icon(Icons.info_outline, color: sage),
|
|
SizedBox(width: 13),
|
|
Expanded(
|
|
child: Text(
|
|
'Ez a szinkron egyir\u00e1ny\u00fa: az eszk\u00f6z lok\u00e1lis napt\u00e1rt\u00e1r\u00e1b\u00f3l olvas, a beolvasott esem\u00e9nyeket JSON cache-be menti, \u00e9s nem \u00edr vissza k\u00fcls\u0151 napt\u00e1rba. '
|
|
'Google, Apple/iCloud, Outlook vagy m\u00e1s napt\u00e1r akkor jelenik meg itt, ha az oper\u00e1ci\u00f3s rendszer napt\u00e1rt\u00e1r\u00e1ban el\u00e9rhet\u0151.',
|
|
style: TextStyle(color: ink, height: 1.45),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _connectGoogle(BuildContext context) async {
|
|
final email = await showDialog<String>(
|
|
context: context,
|
|
builder: (context) =>
|
|
GoogleAccountDialog(initialEmail: connection?.accountEmail ?? ''),
|
|
);
|
|
if (email == null || email.isEmpty) return;
|
|
|
|
widget.onChanged([
|
|
GoogleCalendarConnection(
|
|
id: connection?.id ?? uniqueId(),
|
|
accountEmail: email,
|
|
enabledCalendarIds: connection?.enabledCalendarIds ?? const [],
|
|
lastSync: DateTime.now(),
|
|
),
|
|
]);
|
|
}
|
|
}
|
|
|
|
class GoogleSyncHero extends StatelessWidget {
|
|
const GoogleSyncHero({
|
|
super.key,
|
|
required this.accountEmail,
|
|
required this.enabledCount,
|
|
required this.cachedCount,
|
|
required this.uncategorizedCount,
|
|
});
|
|
|
|
final String? accountEmail;
|
|
final int enabledCount;
|
|
final int cachedCount;
|
|
final int uncategorizedCount;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final connected = accountEmail != null && accountEmail!.isNotEmpty;
|
|
return Container(
|
|
padding: const EdgeInsets.all(22),
|
|
decoration: BoxDecoration(
|
|
gradient: const LinearGradient(
|
|
colors: [Color(0xFF18332D), Color(0xFF2F6B5B)],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
borderRadius: BorderRadius.circular(28),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: sage.withValues(alpha: .22),
|
|
blurRadius: 28,
|
|
offset: const Offset(0, 16),
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 58,
|
|
height: 58,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withValues(alpha: .16),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: const Icon(
|
|
Icons.g_mobiledata_rounded,
|
|
color: Colors.white,
|
|
size: 44,
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
connected
|
|
? accountEmail!
|
|
: 'M\u00e9g nincs napt\u00e1rkapcsolat',
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.w900,
|
|
fontSize: 20,
|
|
),
|
|
),
|
|
const SizedBox(height: 5),
|
|
Text(
|
|
connected
|
|
? '$enabledCount napt\u00e1r bekapcsolva \u00b7 $cachedCount esem\u00e9ny cache-ben \u00b7 $uncategorizedCount kategoriz\u00e1latlan'
|
|
: 'Adj meg egy c\u00edmk\u00e9t, majd v\u00e1laszd ki, mely eszk\u00f6znapt\u00e1rakb\u00f3l hozzon esem\u00e9nyeket.',
|
|
style: const TextStyle(color: Colors.white70, height: 1.35),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class GoogleAccountDialog extends StatefulWidget {
|
|
const GoogleAccountDialog({super.key, required this.initialEmail});
|
|
|
|
final String initialEmail;
|
|
|
|
@override
|
|
State<GoogleAccountDialog> createState() => _GoogleAccountDialogState();
|
|
}
|
|
|
|
class _GoogleAccountDialogState extends State<GoogleAccountDialog> {
|
|
late final TextEditingController controller;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
controller = TextEditingController(
|
|
text: widget.initialEmail.isEmpty
|
|
? 'csalad@gmail.com'
|
|
: widget.initialEmail,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AlertDialog(
|
|
title: const Text('Napt\u00e1rkapcsolat c\u00edmk\u00e9je'),
|
|
content: TextField(
|
|
controller: controller,
|
|
autofocus: true,
|
|
keyboardType: TextInputType.emailAddress,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Fi\u00f3k / kapcsolat neve',
|
|
helperText: 'Pl. csalad@gmail.com, iCloud, munkahelyi napt\u00e1r',
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: const Text('M\u00e9gse'),
|
|
),
|
|
FilledButton(
|
|
onPressed: () => Navigator.pop(context, controller.text.trim()),
|
|
child: const Text('Ment\u00e9s'),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class EmptyGoogleConnectionCard extends StatelessWidget {
|
|
const EmptyGoogleConnectionCard({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return const Card(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(20),
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.calendar_month_outlined, color: sage),
|
|
SizedBox(width: 14),
|
|
Expanded(
|
|
child: Text(
|
|
'Adj hozz\u00e1 egy Google fi\u00f3kot, majd v\u00e1laszd ki, mely napt\u00e1rak jelenjenek meg a helyi csal\u00e1di n\u00e9zetben.',
|
|
style: TextStyle(color: ink, height: 1.4),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class GoogleCalendarToggleCard extends StatelessWidget {
|
|
const GoogleCalendarToggleCard({
|
|
super.key,
|
|
required this.calendar,
|
|
required this.enabled,
|
|
required this.onChanged,
|
|
});
|
|
|
|
final GoogleCalendarOption calendar;
|
|
final bool enabled;
|
|
final ValueChanged<bool> onChanged;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Card(
|
|
child: SwitchListTile(
|
|
value: enabled,
|
|
activeThumbColor: calendar.color,
|
|
onChanged: onChanged,
|
|
secondary: CircleAvatar(
|
|
backgroundColor: calendar.color.withValues(alpha: .14),
|
|
child: Icon(Icons.calendar_today, color: calendar.color, size: 19),
|
|
),
|
|
title: Text(
|
|
calendar.name,
|
|
style: const TextStyle(color: ink, fontWeight: FontWeight.w800),
|
|
),
|
|
subtitle: Text(calendar.description),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class DeviceCalendarToggleCard extends StatelessWidget {
|
|
const DeviceCalendarToggleCard({
|
|
super.key,
|
|
required this.calendar,
|
|
required this.enabled,
|
|
required this.people,
|
|
required this.selectedPersonId,
|
|
required this.onChanged,
|
|
required this.onPersonChanged,
|
|
});
|
|
|
|
final device_calendar.Calendar calendar;
|
|
final bool enabled;
|
|
final List<Person> people;
|
|
final String? selectedPersonId;
|
|
final ValueChanged<bool> onChanged;
|
|
final ValueChanged<String?> onPersonChanged;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final calendarColor = Color(
|
|
calendar.color ?? const Color(0xFF4285F4).toARGB32(),
|
|
);
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(bottom: 10),
|
|
child: Column(
|
|
children: [
|
|
SwitchListTile(
|
|
value: enabled,
|
|
activeThumbColor: calendarColor,
|
|
onChanged: onChanged,
|
|
secondary: CircleAvatar(
|
|
backgroundColor: calendarColor.withValues(alpha: .14),
|
|
child: Icon(
|
|
Icons.calendar_today,
|
|
color: calendarColor,
|
|
size: 19,
|
|
),
|
|
),
|
|
title: Text(
|
|
calendar.name ?? 'N\u00e9vtelen napt\u00e1r',
|
|
style: const TextStyle(color: ink, fontWeight: FontWeight.w800),
|
|
),
|
|
subtitle: Text(
|
|
[
|
|
calendar.accountName,
|
|
calendar.accountType,
|
|
if (calendar.isReadOnly == true) 'csak olvashat\u00f3',
|
|
]
|
|
.whereType<String>()
|
|
.where((part) => part.isNotEmpty)
|
|
.join(' \u00b7 '),
|
|
),
|
|
),
|
|
if (enabled)
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(72, 0, 16, 0),
|
|
child: DropdownButtonFormField<String?>(
|
|
initialValue: selectedPersonId,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Kihez tartozzanak az esem\u00e9nyek?',
|
|
),
|
|
items: [
|
|
const DropdownMenuItem<String?>(
|
|
value: null,
|
|
child: Text('Kategoriz\u00e1latlan'),
|
|
),
|
|
const DropdownMenuItem<String?>(
|
|
value: everyonePersonId,
|
|
child: Text('Mindenki'),
|
|
),
|
|
...people.map(
|
|
(person) => DropdownMenuItem<String?>(
|
|
value: person.id,
|
|
child: Text('${person.avatar} ${person.name}'),
|
|
),
|
|
),
|
|
],
|
|
onChanged: onPersonChanged,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class InfoBox extends StatelessWidget {
|
|
const InfoBox({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(18),
|
|
decoration: BoxDecoration(
|
|
color: mint,
|
|
borderRadius: BorderRadius.circular(18),
|
|
),
|
|
child: const Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Icon(Icons.lock_outline, color: sage),
|
|
SizedBox(width: 13),
|
|
Expanded(
|
|
child: Text(
|
|
'A hiteles\u00edt\u00e9si adatok nem ker\u00fclnek az alkalmaz\u00e1sba. '
|
|
'A Google OAuth, az iCloud pedig biztons\u00e1gos CalDAV-kapcsolaton kereszt\u00fcl csatlakozik.',
|
|
style: TextStyle(color: ink, height: 1.45),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class AccountCard extends StatefulWidget {
|
|
const AccountCard({
|
|
super.key,
|
|
required this.icon,
|
|
required this.color,
|
|
required this.title,
|
|
required this.subtitle,
|
|
});
|
|
|
|
final IconData icon;
|
|
final Color color;
|
|
final String title;
|
|
final String subtitle;
|
|
|
|
@override
|
|
State<AccountCard> createState() => _AccountCardState();
|
|
}
|
|
|
|
class _AccountCardState extends State<AccountCard> {
|
|
bool connected = false;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(18),
|
|
child: Row(
|
|
children: [
|
|
CircleAvatar(
|
|
backgroundColor: widget.color.withValues(alpha: .1),
|
|
child: Icon(widget.icon, color: widget.color, size: 28),
|
|
),
|
|
const SizedBox(width: 15),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
widget.title,
|
|
style: const TextStyle(
|
|
color: ink,
|
|
fontSize: 17,
|
|
fontWeight: FontWeight.w800,
|
|
),
|
|
),
|
|
const SizedBox(height: 3),
|
|
Text(
|
|
connected
|
|
? 'Csatlakoztatva \u00b7 Most friss\u00edtve'
|
|
: widget.subtitle,
|
|
style: TextStyle(color: connected ? sage : Colors.black54),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
OutlinedButton(
|
|
onPressed: () {
|
|
setState(() => connected = !connected);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
connected
|
|
? '${widget.title} dem\u00f3kapcsolat l\u00e9trehozva'
|
|
: '${widget.title} lev\u00e1lasztva',
|
|
),
|
|
),
|
|
);
|
|
},
|
|
child: Text(
|
|
connected ? 'Lev\u00e1laszt\u00e1s' : 'Kapcsol\u00f3d\u00e1s',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|