diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9ac52e6..a1806df 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,7 +51,6 @@ jobs: name: Smarthome ${{ github.ref }} draft: true prerelease: false - generateReleaseNotes: true create-build: @@ -176,3 +175,4 @@ jobs: prerelease: false allowUpdates: true artifacts: smarthome_${{ matrix.target }}${{ matrix.asset_extension }} + generateReleaseNotes: true diff --git a/lib/devices/device_manager.dart b/lib/devices/device_manager.dart index 38c44fb..89a01f3 100644 --- a/lib/devices/device_manager.dart +++ b/lib/devices/device_manager.dart @@ -22,6 +22,7 @@ import 'package:smarthome/helper/iterable_extensions.dart'; import 'package:smarthome/helper/preference_manager.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/legacy.dart'; +import 'package:smarthome/main.dart'; import '../icons/smarthome_icons.dart'; import 'device_exporter.dart'; @@ -400,6 +401,9 @@ class DeviceManager extends _$DeviceManager { connection.state != HubConnectionState.Connected) { return current; } + final firebaseInitialized = await ref.read( + firebaseInitializedProvider.future, + ); if (diffIds.any((final element) => element.action == Action.added)) { final deviceIds = diffIds @@ -411,7 +415,7 @@ class DeviceManager extends _$DeviceManager { for (final id in deviceIds) { if (PreferencesManager.instance.containsKey("SHD$id")) continue; PreferencesManager.instance.setInt("SHD$id", id); - if (Platform.isAndroid || Platform.isIOS) { + if (firebaseInitialized && (Platform.isAndroid || Platform.isIOS)) { FirebaseMessaging.instance.subscribeToTopic("device_$id"); } } @@ -425,7 +429,7 @@ class DeviceManager extends _$DeviceManager { for (final diffId in diffIds) { current.removeWhere((final d) => d.id == diffId.id); - if (Platform.isAndroid || Platform.isIOS) { + if (firebaseInitialized && (Platform.isAndroid || Platform.isIOS)) { FirebaseMessaging.instance.unsubscribeFromTopic("device_$diffId"); } } diff --git a/lib/devices/generic/widgets/edits/basic_edit_types.dart b/lib/devices/generic/widgets/edits/basic_edit_types.dart index d71fccd..d36dc01 100644 --- a/lib/devices/generic/widgets/edits/basic_edit_types.dart +++ b/lib/devices/generic/widgets/edits/basic_edit_types.dart @@ -12,13 +12,24 @@ import 'package:smarthome/helper/connection_manager.dart'; import 'package:smarthome/restapi/swagger.swagger.dart'; class BasicIcon extends ConsumerWidget { - const BasicIcon({super.key, required this.info}); + const BasicIcon({ + super.key, + required this.id, + this.valueModel, + required this.info, + }); final LayoutBasePropertyInfo info; + final int id; + final ValueStore? valueModel; @override Widget build(final BuildContext context, final WidgetRef ref) { if (info.editInfo == null) return const SizedBox(); - final edit = GenericDevice.getEditParameter(null, info.editInfo!, "icon"); + final edit = GenericDevice.getEditParameter( + valueModel, + info.editInfo!, + "icon", + ); if (edit == null) return const SizedBox(); final raw = edit.extensionData ?? {}; final color = raw["Color"] as int?; @@ -59,7 +70,7 @@ class BasicButton extends ConsumerWidget { final tempSettingsDialog = editInfo.dialog == "HeaterConfig"; final child = Row( children: [ - BasicIcon(info: info), + BasicIcon(id: id, valueModel: valueModel, info: info), Text( editInfo.display!, style: TextStyle( @@ -128,7 +139,7 @@ class BasicDropdown extends ConsumerWidget { final editInfo = info.editInfo!; return Row( children: [ - BasicIcon(info: info), + BasicIcon(id: id, valueModel: valueModel, info: info), DropdownButton( items: editInfo.editParameter .map( @@ -221,7 +232,7 @@ class BasicToggle extends ConsumerWidget { return Row( mainAxisSize: MainAxisSize.min, children: [ - BasicIcon(info: info), + BasicIcon(id: id, valueModel: valueModel, info: info), if (edit.displayName != null) Text(edit.displayName!), Switch( onChanged: ((final _) async { @@ -310,7 +321,7 @@ class BasicSlider extends HookConsumerWidget { return Row( children: [ - BasicIcon(info: info), + BasicIcon(id: id, valueModel: valueModel, info: info), SliderTheme( data: sliderTheme, child: Slider( diff --git a/lib/devices/generic_device.dart b/lib/devices/generic_device.dart index c7de1d1..21cd1ce 100644 --- a/lib/devices/generic_device.dart +++ b/lib/devices/generic_device.dart @@ -118,7 +118,7 @@ class GenericDevice extends Device { case "iconbutton": return BasicIconButton(id: id, valueModel: valueModel, info: e); case "icon": - return BasicIcon(info: e); + return BasicIcon(id: id, valueModel: valueModel, info: e); case "radial": return GaugeEdit(id: id, valueModel: valueModel, info: e); // case EditType.input: diff --git a/lib/devices/heater/heater_temp_settings.dart b/lib/devices/heater/heater_temp_settings.dart index a5000ab..4f1f95b 100644 --- a/lib/devices/heater/heater_temp_settings.dart +++ b/lib/devices/heater/heater_temp_settings.dart @@ -86,7 +86,7 @@ class HeaterTempSettings extends HookWidget { final configs = []; final tod = selectedDate; for (int i = 0; i < 7; i++) { - final flag = 1 << (i + 1); + final flag = 1 << i; if (selected & flag == 0) continue; final dayOfWeek = flagToDayOfWeekMap[flag]!; diff --git a/lib/helper/notification_service.dart b/lib/helper/notification_service.dart index 7094a9c..24c6e52 100644 --- a/lib/helper/notification_service.dart +++ b/lib/helper/notification_service.dart @@ -10,6 +10,7 @@ import 'package:smarthome/helper/connection_manager.dart'; import 'package:smarthome/helper/iterable_extensions.dart'; import 'package:smarthome/helper/number_extensions.dart'; import 'package:smarthome/helper/preference_manager.dart'; +import 'package:smarthome/main.dart'; import 'package:smarthome/models/notification_topic.dart'; import 'package:smarthome/notifications/app_notification.dart'; @@ -32,7 +33,7 @@ class NotificationService extends _$NotificationService { static int notificationId = 0; static final Map> - _callbacks = {}; + _callbacks = {}; @override Future build() async { @@ -54,8 +55,9 @@ class NotificationService extends _$NotificationService { void processNotification(final List? arguments) { final notification = AppNotification.fromJson(arguments!.first as dynamic); final topics = PreferencesManager.instance.getNotificationTopics(); - final topic = - topics.firstOrDefault((final x) => x.topic == notification.topic); + final topic = topics.firstOrDefault( + (final x) => x.topic == notification.topic, + ); if (topic == null) return; if (notification.wasOneTime) { final idx = topics.indexOf(topic); @@ -65,7 +67,11 @@ class NotificationService extends _$NotificationService { if (notification is VisibleAppNotification) { flutterLocalNotificationsPlugin.show( - notificationId++, notification.title, notification.body, null); + notificationId++, + notification.title, + notification.body, + null, + ); } final callbacks = _callbacks[notification.runtimeType]; @@ -77,17 +83,22 @@ class NotificationService extends _$NotificationService { } static String registerCallback( - final String id, final Function(T parameter) func) { + final String id, + final Function(T parameter) func, + ) { return _registerCallback(id, T, (final p) => func(p)); } - static String _registerCallback(final String id, final Type type, - final Function(dynamic parameter) func) { + static String _registerCallback( + final String id, + final Type type, + final Function(dynamic parameter) func, + ) { if (_callbacks.containsKey(type)) { _callbacks[type]!.addAll({id: func}); } else { _callbacks.addAll({ - type: {id: func} + type: {id: func}, }); } @@ -121,63 +132,72 @@ class NotificationService extends _$NotificationService { for (final grp in grouped.entries) { final widgets = []; for (final (_, id, element) in grp.value) { - final topic = useState(topics.firstWhere( - (final x) => - x.uniqueName == element.uniqueName && x.deviceId == id, - orElse: () { - final oneTime = element.times == 1; - final String topic; - if (oneTime) { - topic = ""; - } else if (id == null) { - topic = element.uniqueName; - } else { - topic = "${element.uniqueName}_${id.toHex()}"; - } - return NotificationTopic( + final topic = useState( + topics.firstWhere( + (final x) => + x.uniqueName == element.uniqueName && x.deviceId == id, + orElse: () { + final oneTime = element.times == 1; + final String topic; + if (oneTime) { + topic = ""; + } else if (id == null) { + topic = element.uniqueName; + } else { + topic = "${element.uniqueName}_${id.toHex()}"; + } + return NotificationTopic( enabled: false, deviceId: id, oneTime: oneTime, topic: topic, - uniqueName: element.uniqueName); - }, - )); + uniqueName: element.uniqueName, + ); + }, + ), + ); if (!topics.contains(topic.value)) topics.add(topic.value); final checked = useState(topic.value.enabled); - widgets.add(CheckboxListTile( - value: checked.value, - title: Text(element.translatableName), - onChanged: (final value) async { - checked.value = value ?? false; - if (topic.value.topic == "") { - final res = await api.notificationNextNotificationIdGet( - uniqueName: topic.value.uniqueName, - deviceId: id, - ); - final notificationTopic = res.bodyOrThrow; - print(notificationTopic); - final idx = topics.indexOf(topic.value); - topic.value = topic.value.copyWith( - topic: notificationTopic, enabled: checked.value); - topics[idx] = topic.value; - } else if (checked.value) { - final idx = topics.indexOf(topic.value); - topic.value = topic.value.copyWith(enabled: checked.value); - topics[idx] = topic.value; - } else { - final idx = topics.indexOf(topic.value); - topic.value = topic.value.copyWith(enabled: checked.value); - topics[idx] = topic.value; - } - }, - )); + widgets.add( + CheckboxListTile( + value: checked.value, + title: Text(element.translatableName), + onChanged: (final value) async { + checked.value = value ?? false; + if (topic.value.topic == "") { + final res = await api.notificationNextNotificationIdGet( + uniqueName: topic.value.uniqueName, + deviceId: id, + ); + final notificationTopic = res.bodyOrThrow; + print(notificationTopic); + final idx = topics.indexOf(topic.value); + topic.value = topic.value.copyWith( + topic: notificationTopic, + enabled: checked.value, + ); + topics[idx] = topic.value; + } else if (checked.value) { + final idx = topics.indexOf(topic.value); + topic.value = topic.value.copyWith(enabled: checked.value); + topics[idx] = topic.value; + } else { + final idx = topics.indexOf(topic.value); + topic.value = topic.value.copyWith(enabled: checked.value); + topics[idx] = topic.value; + } + }, + ), + ); } - expansionTiles.add(ExpansionTile( - title: Text(grp.key), - initiallyExpanded: grouped.length == 1, - children: widgets, - )); + expansionTiles.add( + ExpansionTile( + title: Text(grp.key), + initiallyExpanded: grouped.length == 1, + children: widgets, + ), + ); } return SingleChildScrollView( @@ -194,26 +214,32 @@ class NotificationService extends _$NotificationService { content: hookedBuilder, actions: [ TextButton( - child: Text("Abbrechen"), - onPressed: () { - Navigator.pop(context, false); - }), + child: Text("Abbrechen"), + onPressed: () { + Navigator.pop(context, false); + }, + ), TextButton( - child: Text("Speichern"), - onPressed: () { - Navigator.pop(context, true); - }) + child: Text("Speichern"), + onPressed: () { + Navigator.pop(context, true); + }, + ), ], ); - final dialogRes = - await showDialog(context: context, builder: (final ctx) => ad); + final dialogRes = await showDialog( + context: context, + builder: (final ctx) => ad, + ); if (dialogRes == true) { final validTopics = topics .where((final x) => x.topic.isNotEmpty && (!x.oneTime || x.enabled)) .toList(); PreferencesManager.instance.setNotificationTopics(validTopics); - if (Platform.isAndroid || Platform.isIOS) { + + final init = await ref.watch(firebaseInitializedProvider.future); + if (init && (Platform.isAndroid || Platform.isIOS)) { for (final topic in validTopics) { if (topic.enabled) { FirebaseMessaging.instance.subscribeToTopic(topic.topic); diff --git a/lib/main.dart b/lib/main.dart index 1a6c2ab..94331af 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -31,13 +31,19 @@ import 'my_app.dart'; import 'package:firebase_core/firebase_core.dart' show Firebase, FirebaseOptions; import 'package:flutter/foundation.dart' - show TargetPlatform, defaultTargetPlatform, kDebugMode, kIsWeb; + show + TargetPlatform, + defaultTargetPlatform, + kDebugMode, + kIsWeb, + kProfileMode; part 'main.g.dart'; @pragma('vm:entry-point') Future _firebaseMessagingBackgroundHandler( - final RemoteMessage message) async { + final RemoteMessage message, +) async { print("Handling a background message: ${message.messageId}"); print(message.toMap().entries.join(',')); print(message.data); @@ -49,10 +55,13 @@ Future _firebaseMessagingBackgroundHandler( //Implement https://developer.android.com/develop/ui/views/device-control as a seperate build //Because having support for android kitkat is still wanted -final _brightnessChangeProvider = ChangeNotifierProvider.family< - AdaptiveThemeModeWatcher, AdaptiveThemeManager>((final ref, final b) { - return AdaptiveThemeModeWatcher(b); -}); +final _brightnessChangeProvider = + ChangeNotifierProvider.family< + AdaptiveThemeModeWatcher, + AdaptiveThemeManager + >((final ref, final b) { + return AdaptiveThemeModeWatcher(b); + }); @riverpod Brightness brightness(final Ref ref, final AdaptiveThemeManager b) { @@ -105,10 +114,11 @@ Widget getTitleWidget(final Ref ref) { if (!enabled) return const Text("Smart Home App"); return TextField( - decoration: const InputDecoration(hintText: "Suche"), - // onSubmitted: (x) => _searchProducts(x, 1), - autofocus: true, - onChanged: (final s) => ref.watch(searchTextProvider.notifier).state = s); + decoration: const InputDecoration(hintText: "Suche"), + // onSubmitted: (x) => _searchProducts(x, 1), + autofocus: true, + onChanged: (final s) => ref.watch(searchTextProvider.notifier).state = s, + ); } @riverpod @@ -119,24 +129,26 @@ List filteredDevices(final Ref ref) { if (searchText.isEmpty) return devices; return devices - .where((final element) => - element.typeName.toLowerCase().contains(searchText) || - (ref - .read(element.baseModelTProvider(element.id)) - ?.friendlyName - .toLowerCase() - .contains(searchText) ?? - false)) + .where( + (final element) => + element.typeName.toLowerCase().contains(searchText) || + (ref + .read(element.baseModelTProvider(element.id)) + ?.friendlyName + .toLowerCase() + .contains(searchText) ?? + false), + ) .toList(); } class CustomScrollBehavior extends MaterialScrollBehavior { @override Set get dragDevices => { - PointerDeviceKind.touch, - PointerDeviceKind.mouse, - PointerDeviceKind.stylus, - }; + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + PointerDeviceKind.stylus, + }; } class DevHttpOverrides extends HttpOverrides { @@ -159,19 +171,21 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); if (kDebugMode) { SharedPreferences.setPrefix("debug."); - } else { - SharedPreferences.setPrefix("test."); + } else if (kProfileMode) { + SharedPreferences.setPrefix("profile."); } + final prefs = await SharedPreferences.getInstance(); // prefs.clear(); - final String certificate = - await rootBundle.loadString('assets/certs/Smarthome.pem'); + final String certificate = await rootBundle.loadString( + 'assets/certs/Smarthome.pem', + ); HttpOverrides.global = DevHttpOverrides(certificate); PreferencesManager.instance = PreferencesManager(prefs); -// initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project + // initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher'); final DarwinInitializationSettings initializationSettingsDarwin = @@ -180,10 +194,10 @@ void main() async { LinuxInitializationSettings(defaultActionName: 'Open notification'); const WindowsInitializationSettings initSettings = WindowsInitializationSettings( - appName: 'Smarthome', - appUserModelId: 'de.susch19.Smarthome', - guid: '07DCF00D-5E87-4882-9F37-A5F3242BF1A1', - ); + appName: 'Smarthome', + appUserModelId: 'de.susch19.Smarthome', + guid: '07DCF00D-5E87-4882-9F37-A5F3242BF1A1', + ); final InitializationSettings initializationSettings = InitializationSettings( android: initializationSettingsAndroid, @@ -205,15 +219,15 @@ void main() async { Intl.defaultLocale = "de-DE"; initializeDateFormatting("de-DE").then((final _) { // ConnectionManager.startConnection(); - runApp(ProviderScope( - child: OKToast( - backgroundColor: Colors.grey.withOpacity(0.3), - position: ToastPosition.bottom, - child: _EagerInitialization( - child: MyApp(savedThemeMode), + runApp( + ProviderScope( + child: OKToast( + backgroundColor: Colors.grey.withOpacity(0.3), + position: ToastPosition.bottom, + child: _EagerInitialization(child: MyApp(savedThemeMode)), ), ), - )); + ); }); UpdateManager.initialize(); } @@ -246,9 +260,11 @@ final infoIconProvider = StateNotifierProvider( (final ref) => InfoIconProvider(ref), ); -final maxCrossAxisExtentProvider = StateProvider((final _) => - PreferencesManager.instance.getDouble("DashboardCardSize") ?? - (!kIsWeb && Platform.isAndroid ? 370 : 300)); +final maxCrossAxisExtentProvider = StateProvider( + (final _) => + PreferencesManager.instance.getDouble("DashboardCardSize") ?? + (!kIsWeb && Platform.isAndroid ? 370 : 300), +); class _EagerInitialization extends ConsumerWidget { const _EagerInitialization({required this.child}); @@ -262,9 +278,7 @@ class _EagerInitialization extends ConsumerWidget { if (value == null) { return; } - await Firebase.initializeApp( - options: value, - ); + await Firebase.initializeApp(options: value); await FirebaseMessaging.instance.requestPermission(); FirebaseMessaging.onMessage.listen((final element) { @@ -282,23 +296,32 @@ class _EagerInitialization extends ConsumerWidget { } } -final _firebaseConfigProvider = - FutureProvider((final ref) async { +final _firebaseConfigProvider = FutureProvider(( + final ref, +) async { final connection = ref.watch(apiProvider); final apiRes = await connection.notificationFirebaseOptionsGet(); final res = apiRes.bodyOrThrow as Map; - final plattformName = - kIsWeb ? "web" : defaultTargetPlatform.name.toLowerCase(); + final plattformName = kIsWeb + ? "web" + : defaultTargetPlatform.name.toLowerCase(); if (!res.containsKey(plattformName)) return null; final subMap = res[plattformName] as Map; return FirebaseOptions( - apiKey: subMap["apiKey"]!, - appId: subMap["appId"]!, - messagingSenderId: subMap["messagingSenderId"]!, - projectId: subMap["projectId"]!, - storageBucket: subMap["storageBucket"], - iosBundleId: subMap["iosBundleId"], - authDomain: subMap["authDomain"]); + apiKey: subMap["apiKey"]!, + appId: subMap["appId"]!, + messagingSenderId: subMap["messagingSenderId"]!, + projectId: subMap["projectId"]!, + storageBucket: subMap["storageBucket"], + iosBundleId: subMap["iosBundleId"], + authDomain: subMap["authDomain"], + ); }); + +@riverpod +FutureOr firebaseInitialized(Ref ref) async { + final val = await ref.watch(_firebaseConfigProvider.future); + return val != null; +} diff --git a/lib/restapi/swagger.enums.swagger.dart b/lib/restapi/swagger.enums.swagger.dart index 97451b1..43fde64 100644 --- a/lib/restapi/swagger.enums.swagger.dart +++ b/lib/restapi/swagger.enums.swagger.dart @@ -2,6 +2,7 @@ // ignore_for_file: type=lint import 'package:json_annotation/json_annotation.dart'; +import 'package:collection/collection.dart'; enum AppLogLevel { @JsonValue(null) diff --git a/lib/screens/setup_page.dart b/lib/screens/setup_page.dart index fddfd36..3d7c6be 100644 --- a/lib/screens/setup_page.dart +++ b/lib/screens/setup_page.dart @@ -60,6 +60,7 @@ class SetupPage extends HookConsumerWidget { controller: urlController, decoration: InputDecoration( errorText: urlError.value, + label: Text("URL"), border: const OutlineInputBorder()), ),