naptar-es-feladatkezelo/lib/data.dart

727 lines
21 KiB
Dart

part of 'main.dart';
const ink = Color(0xFF18332D);
const sage = Color(0xFF2F6B5B);
const mint = Color(0xFFE7F1EC);
const cream = Color(0xFFF7F5EF);
const orange = Color(0xFFE89552);
const everyonePersonId = '_everyone';
const defaultJarvisApiUrl = 'https://api-test.a-web.hu';
const legacyJarvisApiUrl = 'http://80.211.202.74';
const appVersionLabel = 'v1.0.0+1';
const avatarPresets = [
AvatarPreset('boy', 'Fi\u00fa', '\u{1f9d2}'),
AvatarPreset('girl', 'L\u00e1ny', '\u{1f467}'),
AvatarPreset('man', 'F\u00e9rfi', '\u{1f468}'),
AvatarPreset('woman', 'N\u0151', '\u{1f469}'),
AvatarPreset('dog', 'Kutya', '\u{1f436}'),
AvatarPreset('cat', 'Macska', '\u{1f431}'),
AvatarPreset('snowman', 'H\u00f3ember', '\u2603\ufe0f'),
AvatarPreset('strawberry', 'Eper', '\u{1f353}'),
AvatarPreset('sun', 'Napocska', '\u2600\ufe0f'),
AvatarPreset('flower', 'Vir\u00e1g', '\u{1f338}'),
AvatarPreset('car', 'Aut\u00f3', '\u{1f697}'),
AvatarPreset('rocket', 'Rak\u00e9ta', '\u{1f680}'),
];
const personColors = [
Color(0xFF7189C8),
Color(0xFFE89552),
Color(0xFF9B78B4),
Color(0xFF79A894),
Color(0xFFD65A6F),
Color(0xFFD6A74D),
Color(0xFF4C9BCF),
Color(0xFF6A9F58),
];
class AvatarPreset {
const AvatarPreset(this.id, this.label, this.symbol);
final String id;
final String label;
final String symbol;
}
class AppThemeStyle {
const AppThemeStyle({
required this.id,
required this.name,
required this.description,
required this.seed,
required this.surface,
});
final String id;
final String name;
final String description;
final Color seed;
final Color surface;
}
const appThemes = [
AppThemeStyle(
id: 'sage',
name: 'Zs\u00e1lya otthon',
description: 'Nyugodt, term\u00e9szetes csal\u00e1di alapst\u00edlus.',
seed: sage,
surface: cream,
),
AppThemeStyle(
id: 'berry',
name: 'Eperkr\u00e9m',
description:
'Melegebb, j\u00e1t\u00e9kosabb sz\u00ednek gyerekes csal\u00e1dokhoz.',
seed: Color(0xFFD65A6F),
surface: Color(0xFFFFF5F6),
),
AppThemeStyle(
id: 'sky',
name: 'Tiszta \u00e9g',
description:
'H\u0171v\u00f6sebb, rendezett, napt\u00e1rk\u00f6zpont\u00fa hangulat.',
seed: Color(0xFF4C9BCF),
surface: Color(0xFFF1F7FB),
),
AppThemeStyle(
id: 'sunny',
name: 'Naps\u00e1rga',
description:
'Vil\u00e1gos, energikus, konyhai \u00e9s heti tervez\u0151s \u00e9rzet.',
seed: Color(0xFFD6A74D),
surface: Color(0xFFFFFAED),
),
];
class AppJsonStore {
Future<File> _file() async {
try {
final dir = await getApplicationSupportDirectory();
return File('${dir.path}${Platform.pathSeparator}kozos_ter.json');
} on MissingPluginException {
return File(
'${Directory.systemTemp.path}${Platform.pathSeparator}kozos_ter.json',
);
} on UnsupportedError {
return File(
'${Directory.systemTemp.path}${Platform.pathSeparator}kozos_ter.json',
);
}
}
Future<AppSettings> load() async {
try {
final file = await _file();
if (!await file.exists()) {
final settings = AppSettings(
people: defaultPeople(),
themeId: 'sage',
googleConnections: defaultGoogleConnections(),
syncedEvents: const [],
todoSuggestions: const [],
jarvisSettings: JarvisSettings.defaults(),
);
await save(settings);
return settings;
}
final raw = await file.readAsString();
final json = jsonDecode(raw) as Map<String, dynamic>;
return AppSettings.fromJson(json);
} catch (_) {
return AppSettings(
people: defaultPeople(),
themeId: 'sage',
googleConnections: defaultGoogleConnections(),
syncedEvents: const [],
todoSuggestions: const [],
jarvisSettings: JarvisSettings.defaults(),
);
}
}
Future<void> save(AppSettings settings) async {
final file = await _file();
await file.parent.create(recursive: true);
const encoder = JsonEncoder.withIndent(' ');
await file.writeAsString(encoder.convert(settings.toJson()));
}
}
class AppSettings {
AppSettings({
required this.people,
required this.themeId,
required this.googleConnections,
required this.syncedEvents,
required this.todoSuggestions,
required this.jarvisSettings,
});
final List<Person> people;
final String themeId;
final List<GoogleCalendarConnection> googleConnections;
final List<CalendarEvent> syncedEvents;
final List<String> todoSuggestions;
final JarvisSettings jarvisSettings;
factory AppSettings.fromJson(Map<String, dynamic> json) {
final rawPeople = json['people'];
final rawConnections = json['googleConnections'];
final rawSyncedEvents = json['syncedEvents'];
final rawTodoSuggestions = json['todoSuggestions'];
final rawJarvisSettings = json['jarvisSettings'];
return AppSettings(
people: rawPeople is List
? normalizeDefaultPeople(
rawPeople
.whereType<Map<String, dynamic>>()
.map(Person.fromJson)
.toList(),
)
: defaultPeople(),
themeId: json['themeId'] as String? ?? 'sage',
googleConnections: rawConnections is List
? rawConnections
.whereType<Map<String, dynamic>>()
.map(GoogleCalendarConnection.fromJson)
.toList()
: defaultGoogleConnections(),
syncedEvents: rawSyncedEvents is List
? rawSyncedEvents
.whereType<Map<String, dynamic>>()
.map(CalendarEvent.fromJson)
.toList()
: const [],
todoSuggestions: rawTodoSuggestions is List
? rawTodoSuggestions.whereType<String>().toList()
: const [],
jarvisSettings: rawJarvisSettings is Map<String, dynamic>
? JarvisSettings.fromJson(rawJarvisSettings)
: JarvisSettings.defaults(),
);
}
Map<String, dynamic> toJson() => {
'themeId': themeId,
'people': people.map((person) => person.toJson()).toList(),
'googleConnections': googleConnections
.map((connection) => connection.toJson())
.toList(),
'syncedEvents': syncedEvents.map((event) => event.toJson()).toList(),
'todoSuggestions': todoSuggestions,
'jarvisSettings': jarvisSettings.toJson(),
};
}
class JarvisSettings {
JarvisSettings({
required this.apiUrl,
required this.apiToken,
this.deviceName,
this.deviceId,
this.deviceToken,
this.registrationStatus = 'notRegistered',
});
final String apiUrl;
final String apiToken;
final String? deviceName;
final int? deviceId;
final String? deviceToken;
final String registrationStatus;
factory JarvisSettings.defaults() =>
JarvisSettings(apiUrl: defaultJarvisApiUrl, apiToken: '');
factory JarvisSettings.fromJson(Map<String, dynamic> json) => JarvisSettings(
apiUrl: (json['apiUrl'] as String?) == legacyJarvisApiUrl
? defaultJarvisApiUrl
: json['apiUrl'] as String? ?? JarvisSettings.defaults().apiUrl,
apiToken: json['apiToken'] as String? ?? '',
deviceName: json['deviceName'] as String?,
deviceId: json['deviceId'] as int?,
deviceToken: json['deviceToken'] as String?,
registrationStatus:
json['registrationStatus'] as String? ?? 'notRegistered',
);
JarvisSettings copyWith({
String? apiUrl,
String? apiToken,
String? deviceName,
int? deviceId,
String? deviceToken,
String? registrationStatus,
bool clearDevice = false,
}) {
return JarvisSettings(
apiUrl: apiUrl ?? this.apiUrl,
apiToken: apiToken ?? this.apiToken,
deviceName: clearDevice ? null : deviceName ?? this.deviceName,
deviceId: clearDevice ? null : deviceId ?? this.deviceId,
deviceToken: clearDevice ? null : deviceToken ?? this.deviceToken,
registrationStatus: clearDevice
? 'notRegistered'
: registrationStatus ?? this.registrationStatus,
);
}
Map<String, dynamic> toJson() => {
'apiUrl': apiUrl,
'apiToken': apiToken,
'deviceName': deviceName,
'deviceId': deviceId,
'deviceToken': deviceToken,
'registrationStatus': registrationStatus,
};
}
class GoogleCalendarConnection {
GoogleCalendarConnection({
required this.id,
required this.accountEmail,
required this.enabledCalendarIds,
this.calendarPersonIds = const {},
required this.lastSync,
});
final String id;
final String accountEmail;
final List<String> enabledCalendarIds;
final Map<String, String> calendarPersonIds;
final DateTime? lastSync;
GoogleCalendarConnection copyWith({
String? accountEmail,
List<String>? enabledCalendarIds,
Map<String, String>? calendarPersonIds,
DateTime? lastSync,
}) {
return GoogleCalendarConnection(
id: id,
accountEmail: accountEmail ?? this.accountEmail,
enabledCalendarIds: enabledCalendarIds ?? this.enabledCalendarIds,
calendarPersonIds: calendarPersonIds ?? this.calendarPersonIds,
lastSync: lastSync ?? this.lastSync,
);
}
factory GoogleCalendarConnection.fromJson(Map<String, dynamic> json) {
final rawCalendarIds = json['enabledCalendarIds'];
final rawCalendarPersonIds = json['calendarPersonIds'];
return GoogleCalendarConnection(
id: json['id'] as String? ?? uniqueId(),
accountEmail: json['accountEmail'] as String? ?? '',
enabledCalendarIds: rawCalendarIds is List
? rawCalendarIds.whereType<String>().toList()
: const [],
calendarPersonIds: rawCalendarPersonIds is Map
? rawCalendarPersonIds.map(
(key, value) => MapEntry(key.toString(), value.toString()),
)
: const {},
lastSync: DateTime.tryParse(json['lastSync'] as String? ?? ''),
);
}
Map<String, dynamic> toJson() => {
'id': id,
'accountEmail': accountEmail,
'enabledCalendarIds': enabledCalendarIds,
'calendarPersonIds': calendarPersonIds,
'lastSync': lastSync?.toIso8601String(),
};
}
class GoogleCalendarOption {
const GoogleCalendarOption({
required this.id,
required this.name,
required this.description,
required this.color,
});
final String id;
final String name;
final String description;
final Color color;
}
const availableGoogleCalendars = [
GoogleCalendarOption(
id: 'primary',
name: 'Saj\u00e1t napt\u00e1r',
description: 'A f\u0151 Google Calendar esem\u00e9nyeid.',
color: Color(0xFF4285F4),
),
GoogleCalendarOption(
id: 'family',
name: 'Csal\u00e1di napt\u00e1r',
description:
'K\u00f6z\u00f6s csal\u00e1di programok \u00e9s eml\u00e9keztet\u0151k.',
color: Color(0xFF34A853),
),
GoogleCalendarOption(
id: 'school',
name: 'Ovi / iskola',
description:
'Iskolai, \u00f3vodai \u00e9s k\u00fcl\u00f6n\u00f3r\u00e1s esem\u00e9nyek.',
color: Color(0xFFFBBC05),
),
GoogleCalendarOption(
id: 'work',
name: 'Munka',
description:
'Munkahelyi id\u0151pontok, ha szeretn\u00e9d l\u00e1tni \u0151ket.',
color: Color(0xFF9B78B4),
),
];
List<GoogleCalendarConnection> defaultGoogleConnections() => [];
class Person {
Person({
required this.id,
required this.name,
required this.colorValue,
required this.avatar,
this.nextEventLimit = 1,
});
final String id;
final String name;
final int colorValue;
final String avatar;
final int nextEventLimit;
Color get color => Color(colorValue);
Person copyWith({
String? name,
int? colorValue,
String? avatar,
int? nextEventLimit,
}) {
return Person(
id: id,
name: name ?? this.name,
colorValue: colorValue ?? this.colorValue,
avatar: avatar ?? this.avatar,
nextEventLimit: nextEventLimit ?? this.nextEventLimit,
);
}
factory Person.fromJson(Map<String, dynamic> json) {
return Person(
id: json['id'] as String? ?? uniqueId(),
name: json['name'] as String? ?? 'Csal\u00e1dtag',
colorValue: json['colorValue'] as int? ?? personColors.first.toARGB32(),
avatar: json['avatar'] as String? ?? avatarPresets.first.symbol,
nextEventLimit: json['nextEventLimit'] as int? ?? 1,
);
}
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'colorValue': colorValue,
'avatar': avatar,
'nextEventLimit': nextEventLimit,
};
}
List<Person> defaultPeople() => [];
List<Person> normalizeDefaultPeople(List<Person> current) {
const seededIds = {'zsolt', 'eniko', 'balint', 'emilia', 'noemi'};
return current.where((person) => !seededIds.contains(person.id)).toList();
}
class CalendarEvent {
CalendarEvent(
this.title,
this.start,
this.end,
this.color, {
required this.date,
this.place = '',
this.personId,
this.externalId,
this.calendarId,
this.calendarName,
this.note = '',
this.hasTodos = false,
this.todos = const [],
this.source = EventSource.local,
});
final String title;
final String start;
final String end;
final Color color;
final String place;
final String? personId;
final DateTime date;
final String? externalId;
final String? calendarId;
final String? calendarName;
final String note;
final bool hasTodos;
final List<String> todos;
final EventSource source;
bool get isSynced => source == EventSource.deviceCalendar;
bool get isUncategorized => personId == null;
bool get isForEveryone => personId == everyonePersonId;
CalendarEvent copyWith({
String? title,
String? start,
String? end,
Color? color,
String? place,
String? personId,
DateTime? date,
String? note,
bool? hasTodos,
List<String>? todos,
bool clearPerson = false,
}) {
return CalendarEvent(
title ?? this.title,
start ?? this.start,
end ?? this.end,
color ?? this.color,
date: date ?? this.date,
place: place ?? this.place,
personId: clearPerson ? null : personId ?? this.personId,
externalId: externalId,
calendarId: calendarId,
calendarName: calendarName,
note: note ?? this.note,
hasTodos: hasTodos ?? this.hasTodos,
todos: todos ?? this.todos,
source: source,
);
}
factory CalendarEvent.fromJson(Map<String, dynamic> json) {
return CalendarEvent(
json['title'] as String? ?? 'N\u00e9vtelen esem\u00e9ny',
json['start'] as String? ?? '00:00',
json['end'] as String? ?? '',
Color(json['colorValue'] as int? ?? sage.toARGB32()),
date: DateTime.tryParse(json['date'] as String? ?? '') ?? DateTime.now(),
place: json['place'] as String? ?? '',
personId: json['personId'] as String?,
externalId: json['externalId'] as String?,
calendarId: json['calendarId'] as String?,
calendarName: json['calendarName'] as String?,
note: json['note'] as String? ?? '',
hasTodos: json['hasTodos'] as bool? ?? false,
todos: json['todos'] is List
? (json['todos'] as List).whereType<String>().toList()
: const [],
source: EventSource.values.firstWhere(
(source) => source.name == json['source'],
orElse: () => EventSource.local,
),
);
}
factory CalendarEvent.fromDeviceEvent(
device_calendar.Event event,
device_calendar.Calendar calendar, {
String? personId,
}) {
final startDate = event.start?.toLocal() ?? DateTime.now();
final endDate = event.end?.toLocal();
return CalendarEvent(
event.title?.trim().isNotEmpty == true
? event.title!.trim()
: 'N\u00e9vtelen napt\u00e1resem\u00e9ny',
event.allDay == true ? 'Eg\u00e9sz nap' : formatTime(startDate),
event.allDay == true || endDate == null ? '' : formatTime(endDate),
Color(calendar.color ?? const Color(0xFF4285F4).toARGB32()),
date: DateTime(startDate.year, startDate.month, startDate.day),
place: [
calendar.name ?? 'Napt\u00e1r',
if ((event.location ?? '').trim().isNotEmpty) event.location!.trim(),
].join(' \u00b7 '),
externalId: event.eventId,
calendarId: calendar.id,
calendarName: calendar.name,
personId: personId,
note: event.description ?? '',
source: EventSource.deviceCalendar,
);
}
Map<String, dynamic> toJson() => {
'title': title,
'start': start,
'end': end,
'colorValue': color.toARGB32(),
'date': date.toIso8601String(),
'place': place,
'personId': personId,
'externalId': externalId,
'calendarId': calendarId,
'calendarName': calendarName,
'note': note,
'hasTodos': hasTodos,
'todos': todos,
'source': source.name,
};
}
enum EventSource { local, deviceCalendar }
class ShoppingItem {
ShoppingItem(this.name, this.amount, this.category, {this.done = false});
final String name;
final String amount;
final String category;
bool done;
}
class StockItem {
StockItem(this.name, this.amount, this.unit, this.location, this.expiry);
String name;
int amount;
String unit;
String location;
String expiry;
}
class JarvisProduct {
const JarvisProduct({
required this.barcode,
required this.name,
this.brand,
this.quantity,
this.imageUrl,
this.source,
this.fromCache = false,
});
final String barcode;
final String name;
final String? brand;
final String? quantity;
final String? imageUrl;
final String? source;
final bool fromCache;
factory JarvisProduct.fromJson(Map<String, dynamic> json) => JarvisProduct(
barcode: json['barcode'] as String? ?? '',
name: json['name'] as String? ?? '',
brand: json['brand'] as String?,
quantity: json['quantity'] as String?,
imageUrl: json['imageUrl'] as String?,
source: json['source'] as String?,
fromCache: json['fromCache'] as bool? ?? false,
);
String get displayName {
final parts = [
name,
if (brand != null && brand!.trim().isNotEmpty) brand!,
if (quantity != null && quantity!.trim().isNotEmpty) quantity!,
];
return parts.join(' \u00b7 ');
}
}
class WeatherSnapshot {
const WeatherSnapshot({
required this.locationName,
required this.temperature,
required this.description,
required this.icon,
});
final String locationName;
final double? temperature;
final String description;
final IconData icon;
String get temperatureLabel =>
temperature == null ? '\u2014' : '${temperature!.round()}\u00b0C';
}
class WeatherService {
Future<WeatherSnapshot> loadCurrentWeather() async {
try {
var permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
if (permission == LocationPermission.denied ||
permission == LocationPermission.deniedForever) {
return const WeatherSnapshot(
locationName: 'Helymeghat\u00e1roz\u00e1s n\u00e9lk\u00fcl',
temperature: null,
description: 'id\u0151j\u00e1r\u00e1s nem el\u00e9rhet\u0151',
icon: Icons.location_off_outlined,
);
}
final position = await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.low,
timeLimit: Duration(seconds: 8),
),
);
final locationName = await _locationName(position);
final uri = Uri.https('api.open-meteo.com', '/v1/forecast', {
'latitude': position.latitude.toStringAsFixed(4),
'longitude': position.longitude.toStringAsFixed(4),
'current': 'temperature_2m,weather_code',
});
final response = await http.get(uri).timeout(const Duration(seconds: 8));
if (response.statusCode != 200) {
throw HttpException('weather ${response.statusCode}');
}
final payload = jsonDecode(response.body) as Map<String, dynamic>;
final current = payload['current'] as Map<String, dynamic>?;
final temperature = (current?['temperature_2m'] as num?)?.toDouble();
final code = (current?['weather_code'] as num?)?.toInt();
return WeatherSnapshot(
locationName: locationName,
temperature: temperature,
description: weatherDescription(code),
icon: weatherIcon(code),
);
} catch (_) {
return const WeatherSnapshot(
locationName: 'Offline m\u00f3d',
temperature: null,
description: 'id\u0151j\u00e1r\u00e1s k\u00e9s\u0151bb friss\u00fcl',
icon: Icons.cloud_off_outlined,
);
}
}
Future<String> _locationName(Position position) async {
try {
final places = await geocoding.Geocoding().placemarkFromCoordinates(
position.latitude,
position.longitude,
);
final place = places.firstOrNull;
return [
place?.locality,
if (place?.locality == null) place?.subAdministrativeArea,
].whereType<String>().where((part) => part.isNotEmpty).join(', ');
} catch (_) {
return 'Aktu\u00e1lis hely';
}
}
}