760 lines
22 KiB
Dart
760 lines
22 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.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<String, dynamic> 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<String, dynamic> 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<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 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<String, dynamic> 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<String>()
|
|
.where((part) => part.trim().isNotEmpty)
|
|
.join(' \u00b7 '),
|
|
color: parseGoogleColor(backgroundColor),
|
|
primary: json['primary'] == true,
|
|
);
|
|
}
|
|
}
|
|
|
|
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.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<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.fromGoogleEvent(
|
|
Map<String, dynamic> event,
|
|
GoogleCalendarInfo calendar, {
|
|
String? personId,
|
|
}) {
|
|
final startJson = event['start'] as Map<String, dynamic>? ?? const {};
|
|
final endJson = event['end'] as Map<String, dynamic>? ?? 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<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, 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<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';
|
|
}
|
|
}
|
|
}
|