diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/NotificationsFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/NotificationsFlutterBridge.kt index c24243e6..35072a71 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/NotificationsFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/NotificationsFlutterBridge.kt @@ -3,6 +3,7 @@ package io.rebble.cobble.bridges.common import io.rebble.cobble.bridges.FlutterBridge import io.rebble.cobble.bridges.ui.BridgeLifecycleController import io.rebble.cobble.pigeons.Pigeons +import io.rebble.cobble.shared.database.dao.CachedPackageInfoDao import io.rebble.libpebblecommon.packets.blobdb.PushNotification import io.rebble.libpebblecommon.services.notification.NotificationService import kotlinx.coroutines.CoroutineScope @@ -12,7 +13,8 @@ import javax.inject.Inject class NotificationsFlutterBridge @Inject constructor( bridgeLifecycleController: BridgeLifecycleController, private val notificationService: NotificationService, - private val coroutineScope: CoroutineScope + private val coroutineScope: CoroutineScope, + private val cachedPackageInfoDao: CachedPackageInfoDao, ) : FlutterBridge, Pigeons.NotificationsControl { init { bridgeLifecycleController.setupControl(Pigeons.NotificationsControl::setup, this) @@ -26,4 +28,17 @@ class NotificationsFlutterBridge @Inject constructor( )) } } + + override fun getNotificationPackages(result: Pigeons.Result>) { + coroutineScope.launch { + result.success( + cachedPackageInfoDao.getAll().map { + Pigeons.NotifyingPackage.Builder() + .setPackageId(it.id) + .setPackageName(it.name) + .build() + }.toMutableList() + ) + } + } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/di/AppModule.kt b/android/app/src/main/kotlin/io/rebble/cobble/di/AppModule.kt index 6e3580ab..7f46a2c5 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/di/AppModule.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/di/AppModule.kt @@ -8,6 +8,7 @@ import dagger.Module import dagger.Provides import io.rebble.cobble.errors.GlobalExceptionHandler import io.rebble.cobble.shared.database.AppDatabase +import io.rebble.cobble.shared.database.dao.CachedPackageInfoDao import io.rebble.cobble.shared.database.dao.PersistedNotificationDao import io.rebble.cobble.shared.datastore.KMPPrefs import io.rebble.cobble.shared.datastore.createDataStore @@ -50,5 +51,9 @@ abstract class AppModule { fun providePersistedNotificationDao(context: Context): PersistedNotificationDao { return AppDatabase.instance().persistedNotificationDao() } + @Provides + fun provideCachedPackageInfoDao(context: Context): CachedPackageInfoDao { + return AppDatabase.instance().cachedPackageInfoDao() + } } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java b/android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java index 56c53359..87a7d656 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java +++ b/android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java @@ -2831,6 +2831,79 @@ ArrayList toList() { } } + /** Generated class from Pigeon that represents data sent in messages. */ + public static final class NotifyingPackage { + private @NonNull String packageId; + + public @NonNull String getPackageId() { + return packageId; + } + + public void setPackageId(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"packageId\" is null."); + } + this.packageId = setterArg; + } + + private @NonNull String packageName; + + public @NonNull String getPackageName() { + return packageName; + } + + public void setPackageName(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"packageName\" is null."); + } + this.packageName = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + NotifyingPackage() {} + + public static final class Builder { + + private @Nullable String packageId; + + public @NonNull Builder setPackageId(@NonNull String setterArg) { + this.packageId = setterArg; + return this; + } + + private @Nullable String packageName; + + public @NonNull Builder setPackageName(@NonNull String setterArg) { + this.packageName = setterArg; + return this; + } + + public @NonNull NotifyingPackage build() { + NotifyingPackage pigeonReturn = new NotifyingPackage(); + pigeonReturn.setPackageId(packageId); + pigeonReturn.setPackageName(packageName); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(2); + toListResult.add(packageId); + toListResult.add(packageName); + return toListResult; + } + + static @NonNull NotifyingPackage fromList(@NonNull ArrayList list) { + NotifyingPackage pigeonResult = new NotifyingPackage(); + Object packageId = list.get(0); + pigeonResult.setPackageId((String) packageId); + Object packageName = list.get(1); + pigeonResult.setPackageName((String) packageName); + return pigeonResult; + } + } + public interface Result { @SuppressWarnings("UnknownNullness") void success(T result); @@ -4061,14 +4134,43 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable UiConnecti } } } + + private static class NotificationsControlCodec extends StandardMessageCodec { + public static final NotificationsControlCodec INSTANCE = new NotificationsControlCodec(); + + private NotificationsControlCodec() {} + + @Override + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { + switch (type) { + case (byte) 128: + return NotifyingPackage.fromList((ArrayList) readValue(buffer)); + default: + return super.readValueOfType(type, buffer); + } + } + + @Override + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { + if (value instanceof NotifyingPackage) { + stream.write(128); + writeValue(stream, ((NotifyingPackage) value).toList()); + } else { + super.writeValue(stream, value); + } + } + } + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface NotificationsControl { void sendTestNotification(); + void getNotificationPackages(@NonNull Result> result); + /** The codec used by NotificationsControl. */ static @NonNull MessageCodec getCodec() { - return new StandardMessageCodec(); + return NotificationsControlCodec.INSTANCE; } /**Sets up an instance of `NotificationsControl` to handle messages through the `binaryMessenger`. */ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable NotificationsControl api) { @@ -4094,6 +4196,33 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable Notificati channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.NotificationsControl.getNotificationPackages", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + Result> resultCallback = + new Result>() { + public void success(List result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.getNotificationPackages(resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } } } diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/dao/CachedPackageInfoDao.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/dao/CachedPackageInfoDao.kt index 56ad0d20..892b4c40 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/dao/CachedPackageInfoDao.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/dao/CachedPackageInfoDao.kt @@ -13,4 +13,7 @@ interface CachedPackageInfoDao { @Query("SELECT * FROM CachedPackageInfo WHERE id = :packageId") suspend fun getPackageInfo(packageId: String): CachedPackageInfo? + + @Query("SELECT * FROM CachedPackageInfo") + suspend fun getAll(): List } \ No newline at end of file diff --git a/ios/Runner/Pigeon/Pigeons.h b/ios/Runner/Pigeon/Pigeons.h index fde89360..cab61a9f 100644 --- a/ios/Runner/Pigeon/Pigeons.h +++ b/ios/Runner/Pigeon/Pigeons.h @@ -33,6 +33,7 @@ NS_ASSUME_NONNULL_BEGIN @class AppLogEntry; @class OAuthResult; @class NotifChannelPigeon; +@class NotifyingPackage; /// Pigeon only supports classes as return/receive type. /// That is why we must wrap primitive types into wrapper @@ -326,6 +327,15 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, strong, nullable) NSNumber * delete; @end +@interface NotifyingPackage : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithPackageId:(NSString *)packageId + packageName:(NSString *)packageName; +@property(nonatomic, copy) NSString * packageId; +@property(nonatomic, copy) NSString * packageName; +@end + /// The codec used by ScanCallbacks. NSObject *ScanCallbacksGetCodec(void); @@ -486,6 +496,7 @@ NSObject *NotificationsControlGetCodec(void); @protocol NotificationsControl - (void)sendTestNotificationWithError:(FlutterError *_Nullable *_Nonnull)error; +- (void)getNotificationPackagesWithCompletion:(void (^)(NSArray *_Nullable, FlutterError *_Nullable))completion; @end extern void NotificationsControlSetup(id binaryMessenger, NSObject *_Nullable api); diff --git a/ios/Runner/Pigeon/Pigeons.m b/ios/Runner/Pigeon/Pigeons.m index 0a33a7ea..26465194 100644 --- a/ios/Runner/Pigeon/Pigeons.m +++ b/ios/Runner/Pigeon/Pigeons.m @@ -159,6 +159,12 @@ + (nullable NotifChannelPigeon *)nullableFromList:(NSArray *)list; - (NSArray *)toList; @end +@interface NotifyingPackage () ++ (NotifyingPackage *)fromList:(NSArray *)list; ++ (nullable NotifyingPackage *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + @implementation BooleanWrapper + (instancetype)makeWithValue:(nullable NSNumber *)value { BooleanWrapper* pigeonResult = [[BooleanWrapper alloc] init]; @@ -984,6 +990,33 @@ - (NSArray *)toList { } @end +@implementation NotifyingPackage ++ (instancetype)makeWithPackageId:(NSString *)packageId + packageName:(NSString *)packageName { + NotifyingPackage* pigeonResult = [[NotifyingPackage alloc] init]; + pigeonResult.packageId = packageId; + pigeonResult.packageName = packageName; + return pigeonResult; +} ++ (NotifyingPackage *)fromList:(NSArray *)list { + NotifyingPackage *pigeonResult = [[NotifyingPackage alloc] init]; + pigeonResult.packageId = GetNullableObjectAtIndex(list, 0); + NSAssert(pigeonResult.packageId != nil, @""); + pigeonResult.packageName = GetNullableObjectAtIndex(list, 1); + NSAssert(pigeonResult.packageName != nil, @""); + return pigeonResult; +} ++ (nullable NotifyingPackage *)nullableFromList:(NSArray *)list { + return (list) ? [NotifyingPackage fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.packageId ?: [NSNull null]), + (self.packageName ?: [NSNull null]), + ]; +} +@end + @interface ScanCallbacksCodecReader : FlutterStandardReader @end @implementation ScanCallbacksCodecReader @@ -2320,9 +2353,50 @@ void UiConnectionControlSetup(id binaryMessenger, NSObje } } } +@interface NotificationsControlCodecReader : FlutterStandardReader +@end +@implementation NotificationsControlCodecReader +- (nullable id)readValueOfType:(UInt8)type { + switch (type) { + case 128: + return [NotifyingPackage fromList:[self readValue]]; + default: + return [super readValueOfType:type]; + } +} +@end + +@interface NotificationsControlCodecWriter : FlutterStandardWriter +@end +@implementation NotificationsControlCodecWriter +- (void)writeValue:(id)value { + if ([value isKindOfClass:[NotifyingPackage class]]) { + [self writeByte:128]; + [self writeValue:[value toList]]; + } else { + [super writeValue:value]; + } +} +@end + +@interface NotificationsControlCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation NotificationsControlCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[NotificationsControlCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[NotificationsControlCodecReader alloc] initWithData:data]; +} +@end + NSObject *NotificationsControlGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; - sSharedObject = [FlutterStandardMessageCodec sharedInstance]; + static dispatch_once_t sPred = 0; + dispatch_once(&sPred, ^{ + NotificationsControlCodecReaderWriter *readerWriter = [[NotificationsControlCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); return sSharedObject; } @@ -2344,6 +2418,23 @@ void NotificationsControlSetup(id binaryMessenger, NSObj [channel setMessageHandler:nil]; } } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.NotificationsControl.getNotificationPackages" + binaryMessenger:binaryMessenger + codec:NotificationsControlGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(getNotificationPackagesWithCompletion:)], @"NotificationsControl api (%@) doesn't respond to @selector(getNotificationPackagesWithCompletion:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + [api getNotificationPackagesWithCompletion:^(NSArray *_Nullable output, FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } } @interface IntentControlCodecReader : FlutterStandardReader @end diff --git a/lib/infrastructure/pigeons/pigeons.g.dart b/lib/infrastructure/pigeons/pigeons.g.dart index b87b9a64..f095cdec 100644 --- a/lib/infrastructure/pigeons/pigeons.g.dart +++ b/lib/infrastructure/pigeons/pigeons.g.dart @@ -912,6 +912,32 @@ class NotifChannelPigeon { } } +class NotifyingPackage { + NotifyingPackage({ + required this.packageId, + required this.packageName, + }); + + String packageId; + + String packageName; + + Object encode() { + return [ + packageId, + packageName, + ]; + } + + static NotifyingPackage decode(Object result) { + result as List; + return NotifyingPackage( + packageId: result[0]! as String, + packageName: result[1]! as String, + ); + } +} + class _ScanCallbacksCodec extends StandardMessageCodec { const _ScanCallbacksCodec(); @override @@ -2135,6 +2161,29 @@ class UiConnectionControl { } } +class _NotificationsControlCodec extends StandardMessageCodec { + const _NotificationsControlCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is NotifyingPackage) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return NotifyingPackage.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + class NotificationsControl { /// Constructor for [NotificationsControl]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default @@ -2143,7 +2192,7 @@ class NotificationsControl { : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = StandardMessageCodec(); + static const MessageCodec codec = _NotificationsControlCodec(); Future sendTestNotification() async { final BasicMessageChannel channel = BasicMessageChannel( @@ -2166,6 +2215,33 @@ class NotificationsControl { return; } } + + Future> getNotificationPackages() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NotificationsControl.getNotificationPackages', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as List?)!.cast(); + } + } } class _IntentControlCodec extends StandardMessageCodec { diff --git a/lib/main.dart b/lib/main.dart index b79d5a2e..9a933cff 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ +import 'dart:async'; import 'dart:ui'; import 'package:cobble/background/main_background.dart'; @@ -18,6 +19,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:noob/noob.dart'; import 'domain/permissions.dart'; import 'infrastructure/datasources/paired_storage.dart'; @@ -38,6 +40,16 @@ void main() { } }); + /*if (kDebugMode) { + TrackingBuildOwnerWidgetsFlutterBinding.ensureInitialized(); + + // initialize `BuildTracker` + final tracker = BuildTracker(printBuildFrameIncludeRebuildDirtyWidget: false); + + // print top 10 stacks leading to rebuilds every 10 seconds + Timer.periodic(const Duration(seconds: 10), (_) => tracker.printTopScheduleBuildForStacks()); + }*/ + runApp(ProviderScope(child: MyApp())); initBackground(); } diff --git a/lib/ui/screens/alerting_apps.dart b/lib/ui/screens/alerting_apps.dart index 193aea53..312b6496 100644 --- a/lib/ui/screens/alerting_apps.dart +++ b/lib/ui/screens/alerting_apps.dart @@ -27,10 +27,15 @@ class AlertingApp { } class AlertingApps extends HookConsumerWidget implements CobbleScreen { + final notificationsControl = NotificationsControl(); @override Widget build(BuildContext context, WidgetRef ref) { - final packageDetails = ref.watch(packageDetailsProvider).getPackageList(); + var packageDetailsP = ref.watch(packageDetailsProvider); + final notifyingPackagesF = useMemoized(() => notificationsControl.getNotificationPackages()); + final packageDetailsF = useMemoized(() => packageDetailsP.getPackageList()); + final notifyingPackages = useFuture(notifyingPackagesF); + final packageDetails = useFuture(packageDetailsF); final random = Random(); final filter = useState(SheetOnChanged.initial); @@ -38,6 +43,28 @@ class AlertingApps extends HookConsumerWidget implements CobbleScreen { final sheet = CobbleSheet.useInline(); final mutedPackages = ref.watch(notificationsMutedPackagesProvider); + List apps = []; + if (packageDetails.hasData && packageDetails.data != null) { + for (int i = 0; i < packageDetails.data!.packageId!.length; i++) { + final enabled = (mutedPackages.value ?? []).firstWhereOrNull( + (element) => element == packageDetails.data!.packageId![i], + ) == null; + if (notifyingPackages.hasData && + notifyingPackages.data!.firstWhereOrNull((element) => element! + .packageId == packageDetails.data!.packageId![i]) != null) { + apps.add(AlertingApp(packageDetails.data!.appName![i] as String, enabled, + packageDetails.data!.packageId![i] as String)); + } + } + } + + List filteredApps = apps.where( + (app) => + app.name.toLowerCase().contains( + filter.value.query?.toLowerCase() ?? '', + ), + ).toList(); + return CobbleScaffold.tab( title: tr.alertingApps.title, subtitle: tr.alertingApps.subtitle( @@ -73,47 +100,26 @@ class AlertingApps extends HookConsumerWidget implements CobbleScreen { ), ), ], - child: FutureBuilder( - future: packageDetails, - builder: (BuildContext context, - AsyncSnapshot snapshot) { - if (snapshot.hasData && snapshot.data != null) { - List apps = []; - for (int i = 0; i < snapshot.data!.packageId!.length; i++) { - final enabled = (mutedPackages.value ?? []).firstWhereOrNull( - (element) => element == snapshot.data!.packageId![i], - ) == null; - apps.add(AlertingApp(snapshot.data!.appName![i] as String, enabled, - snapshot.data!.packageId![i] as String)); - } - - List filteredApps = apps.where( - (app) => app.name.toLowerCase().contains( - filter.value.query?.toLowerCase() ?? '', - ), - ).toList(); - - return ListView.builder( - itemCount: filteredApps.length, - itemBuilder: (BuildContext context, int index) { - AlertingApp app = filteredApps[index]; - return CobbleTile.appNavigation( - leading: Svg('images/temp_alerting_app.svg'), - title: app.name, - subtitle: app.enabled - ? tr.alertingApps.alertedToday( - alerted: random.nextInt(8).toString(), - ) - : tr.alertingApps.mutedToday( - muted: random.nextInt(8).toString(), - ), - navigateTo: AlertingAppDetails(app), - ); - }, - ); - } else { - return CircularProgressIndicator(); - } - })); + child: packageDetails.hasData && notifyingPackages.hasData ? ListView.builder( + itemCount: filteredApps.length, + itemBuilder: (BuildContext context, int index) { + AlertingApp app = filteredApps[index]; + return CobbleTile.appNavigation( + leading: Svg('images/temp_alerting_app.svg'), + title: app.name, + subtitle: app.enabled + ? tr.alertingApps.alertedToday( + alerted: random.nextInt(8).toString(), + ) + : tr.alertingApps.mutedToday( + muted: random.nextInt(8).toString(), + ), + navigateTo: AlertingAppDetails(app), + ); + }, + ) : const Center( + child: CircularProgressIndicator(), + ) + ); } } diff --git a/pigeons/pigeons.dart b/pigeons/pigeons.dart index 8022e0a7..caab98eb 100644 --- a/pigeons/pigeons.dart +++ b/pigeons/pigeons.dart @@ -189,6 +189,12 @@ class NotifChannelPigeon { bool? delete; } +class NotifyingPackage { + String packageId; + String packageName; + NotifyingPackage(this.packageId, this.packageName); +} + @FlutterApi() abstract class ScanCallbacks { /// pebbles = list of PebbleScanDevicePigeon @@ -315,6 +321,8 @@ abstract class UiConnectionControl { @HostApi() abstract class NotificationsControl { void sendTestNotification(); + @async + List getNotificationPackages(); } @HostApi() diff --git a/pubspec.lock b/pubspec.lock index 193d2996..5690c840 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -454,6 +454,14 @@ packages: description: flutter source: sdk version: "0.0.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + url: "https://pub.dev" + source: hosted + version: "2.4.1" frontend_server_client: dependency: transitive description: @@ -654,6 +662,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.0" + noob: + dependency: "direct main" + description: + name: noob + sha256: c955aff8242224d27c4addce50483b9ac4132425d7ad97bc20974117ce97931b + url: "https://pub.dev" + source: hosted + version: "0.0.8" octo_image: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a2a53028..b3f32e04 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,6 +60,7 @@ dependencies: crypto: ^3.0.3 cached_network_image: ^3.0.0 device_info_plus: ^9.0.0 + noob: ^0.0.8 dev_dependencies: flutter_launcher_icons: ^0.13.1