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() 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 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; return AppSettings.fromJson(json); } catch (_) { return AppSettings( people: defaultPeople(), themeId: 'sage', googleConnections: defaultGoogleConnections(), syncedEvents: const [], todoSuggestions: const [], jarvisSettings: JarvisSettings.defaults(), ); } } Future 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 people; final String themeId; final List googleConnections; final List syncedEvents; final List todoSuggestions; final JarvisSettings jarvisSettings; factory AppSettings.fromJson(Map 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(Person.fromJson) .toList(), ) : defaultPeople(), themeId: json['themeId'] as String? ?? 'sage', googleConnections: rawConnections is List ? rawConnections .whereType>() .map(GoogleCalendarConnection.fromJson) .toList() : defaultGoogleConnections(), syncedEvents: rawSyncedEvents is List ? rawSyncedEvents .whereType>() .map(CalendarEvent.fromJson) .toList() : const [], todoSuggestions: rawTodoSuggestions is List ? rawTodoSuggestions.whereType().toList() : const [], jarvisSettings: rawJarvisSettings is Map ? JarvisSettings.fromJson(rawJarvisSettings) : JarvisSettings.defaults(), ); } Map 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.workspaceToken, this.deviceName, this.deviceId, this.deviceToken, this.registrationStatus = 'notRegistered', }); final String apiUrl; final String apiToken; final String? workspaceToken; final String? deviceName; final int? deviceId; final String? deviceToken; final String registrationStatus; factory JarvisSettings.defaults() => JarvisSettings(apiUrl: defaultJarvisApiUrl, apiToken: ''); factory JarvisSettings.fromJson(Map json) => JarvisSettings( apiUrl: (json['apiUrl'] as String?) == legacyJarvisApiUrl ? defaultJarvisApiUrl : json['apiUrl'] as String? ?? JarvisSettings.defaults().apiUrl, apiToken: json['apiToken'] as String? ?? '', workspaceToken: json['workspaceToken'] 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? workspaceToken, String? deviceName, int? deviceId, String? deviceToken, String? registrationStatus, bool clearDevice = false, }) { return JarvisSettings( apiUrl: apiUrl ?? this.apiUrl, apiToken: apiToken ?? this.apiToken, workspaceToken: workspaceToken ?? this.workspaceToken, 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 toJson() => { 'apiUrl': apiUrl, 'apiToken': apiToken, 'workspaceToken': workspaceToken, '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 enabledCalendarIds; final Map calendarPersonIds; final DateTime? lastSync; GoogleCalendarConnection copyWith({ String? accountEmail, List? enabledCalendarIds, Map? 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 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().toList() : const [], calendarPersonIds: rawCalendarPersonIds is Map ? rawCalendarPersonIds.map( (key, value) => MapEntry(key.toString(), value.toString()), ) : const {}, lastSync: DateTime.tryParse(json['lastSync'] as String? ?? ''), ); } Map toJson() => { 'id': id, 'accountEmail': accountEmail, 'enabledCalendarIds': enabledCalendarIds, 'calendarPersonIds': calendarPersonIds, 'lastSync': lastSync?.toIso8601String(), }; } class GoogleCalendarInfo { const GoogleCalendarInfo({ required this.id, required this.name, required this.color, this.description = '', this.primary = false, }); final String id; final String name; final String description; final Color color; final bool primary; factory GoogleCalendarInfo.fromJson(Map json) { final backgroundColor = json['backgroundColor'] as String?; return GoogleCalendarInfo( id: json['id'] as String? ?? 'primary', name: json['summaryOverride'] as String? ?? json['summary'] as String? ?? 'N\u00e9vtelen Google napt\u00e1r', description: [ json['description'] as String?, json['accessRole'] as String?, ] .whereType() .where((part) => part.trim().isNotEmpty) .join(' \u00b7 '), color: parseGoogleColor(backgroundColor), primary: json['primary'] == true, ); } } List 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 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 toJson() => { 'id': id, 'name': name, 'colorValue': colorValue, 'avatar': avatar, 'nextEventLimit': nextEventLimit, }; } List defaultPeople() => []; List normalizeDefaultPeople(List 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 todos; final EventSource source; bool get isSynced => source == EventSource.googleCalendar; 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? 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 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().toList() : const [], source: EventSource.values.firstWhere( (source) => source.name == json['source'], orElse: () => EventSource.local, ), ); } factory CalendarEvent.fromGoogleEvent( Map event, GoogleCalendarInfo calendar, { String? personId, }) { final startJson = event['start'] as Map? ?? const {}; final endJson = event['end'] as Map? ?? const {}; final startDate = parseGoogleEventDate(startJson) ?? DateTime.now(); final endDate = parseGoogleEventDate(endJson); final allDay = startJson['date'] != null; final title = event['summary'] as String?; final location = event['location'] as String?; final description = event['description'] as String?; return CalendarEvent( title?.trim().isNotEmpty == true ? title!.trim() : 'N\u00e9vtelen Google esem\u00e9ny', allDay ? 'Eg\u00e9sz nap' : formatTime(startDate), allDay || endDate == null ? '' : formatTime(endDate), calendar.color, date: DateTime(startDate.year, startDate.month, startDate.day), place: [ calendar.name, if ((location ?? '').trim().isNotEmpty) location!.trim(), ].join(' \u00b7 '), externalId: event['id'] as String?, calendarId: calendar.id, calendarName: calendar.name, personId: personId, note: description ?? '', source: EventSource.googleCalendar, ); } Map 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, googleCalendar } class ShoppingItem { ShoppingItem(this.name, this.amount, this.category, {this.done = false}); final String name; final String amount; final String category; bool done; factory ShoppingItem.fromJson(Map json) => ShoppingItem( json['name'] as String? ?? '', json['amount'] as String? ?? '1 db', json['category'] as String? ?? 'Egy\u00e9b', done: json['done'] as bool? ?? false, ); Map toJson() => { 'name': name, 'amount': amount, 'category': category, 'done': done, }; } class StockItem { StockItem(this.name, this.amount, this.unit, this.location, this.expiry); String name; int amount; String unit; String location; String expiry; factory StockItem.fromJson(Map json) => StockItem( json['name'] as String? ?? '', json['amount'] as int? ?? 1, json['unit'] as String? ?? 'db', json['location'] as String? ?? 'Kamra', json['expiry'] as String? ?? 'nincs megadva', ); Map toJson() => { 'name': name, 'amount': amount, 'unit': unit, 'location': location, 'expiry': 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 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 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; final current = payload['current'] as Map?; 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 _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().where((part) => part.isNotEmpty).join(', '); } catch (_) { return 'Aktu\u00e1lis hely'; } } }