diff --git a/.github/workflows/app-ci.yaml b/.github/workflows/app-ci.yaml index 5ac6eec..62437c0 100644 --- a/.github/workflows/app-ci.yaml +++ b/.github/workflows/app-ci.yaml @@ -1,8 +1,6 @@ name: "Run Tests and Build for Web/Android/iOS" on: pull_request: - branches: - - main workflow_dispatch: jobs: diff --git a/assets/images/house.png b/assets/images/house.png new file mode 100644 index 0000000..411ef88 Binary files /dev/null and b/assets/images/house.png differ diff --git a/ios/ExportOptions.plist b/ios/ExportOptions.plist index 190d01c..91ac72b 100644 --- a/ios/ExportOptions.plist +++ b/ios/ExportOptions.plist @@ -12,8 +12,6 @@ com.dieklingel.app match AppStore com.dieklingel.app - com.dieklingel.app.ImageNotification - match AppStore com.dieklingel.app.ImageNotification signingCertificate 8702C3CEE167260D632A1AC65DE99063A5FEC4DF diff --git a/ios/ImageNotification/Info.plist b/ios/ImageNotification/Info.plist deleted file mode 100644 index 6d42710..0000000 --- a/ios/ImageNotification/Info.plist +++ /dev/null @@ -1,17 +0,0 @@ - - - - - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - NSExtension - - NSExtensionPointIdentifier - com.apple.usernotifications.service - NSExtensionPrincipalClass - NotificationService - - - diff --git a/ios/ImageNotification/NotificationService.h b/ios/ImageNotification/NotificationService.h deleted file mode 100644 index 65839bf..0000000 --- a/ios/ImageNotification/NotificationService.h +++ /dev/null @@ -1,12 +0,0 @@ -// -// NotificationService.h -// ImageNotification -// -// Created by Kai Mayer on 02.10.22. -// - -#import - -@interface NotificationService : UNNotificationServiceExtension - -@end diff --git a/ios/ImageNotification/NotificationService.m b/ios/ImageNotification/NotificationService.m deleted file mode 100644 index a906af1..0000000 --- a/ios/ImageNotification/NotificationService.m +++ /dev/null @@ -1,38 +0,0 @@ -// -// NotificationService.m -// ImageNotification -// -// Created by Kai Mayer on 02.10.22. -// - -#import -#import "NotificationService.h" -#import "FirebaseMessaging.h" - -@interface NotificationService () - -@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver); -@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent; - -@end - -@implementation NotificationService - -- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler { - self.contentHandler = contentHandler; - self.bestAttemptContent = [request.content mutableCopy]; - - // Modify the notification content here... - // self.bestAttemptContent.title = [NSString stringWithFormat:@"%@ [modified]", self.bestAttemptContent.title]; - // - // self.contentHandler(self.bestAttemptContent); - [[FIRMessaging extensionHelper] populateNotificationContent:self.bestAttemptContent withContentHandler:contentHandler]; -} - -- (void)serviceExtensionTimeWillExpire { - // Called just before the extension will be terminated by the system. - // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. - self.contentHandler(self.bestAttemptContent); -} - -@end diff --git a/ios/Podfile b/ios/Podfile index 1838b30..10f3c9b 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -39,8 +39,3 @@ post_install do |installer| flutter_additional_ios_build_settings(target) end end - -target 'ImageNotification' do - use_frameworks! - pod 'Firebase/Messaging' -end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b07d84c..6f382e2 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,30 +1,30 @@ PODS: - audio_session (0.0.1): - Flutter - - Firebase/CoreOnly (10.12.0): - - FirebaseCore (= 10.12.0) - - Firebase/Messaging (10.12.0): + - Firebase/CoreOnly (10.18.0): + - FirebaseCore (= 10.18.0) + - Firebase/Messaging (10.18.0): - Firebase/CoreOnly - - FirebaseMessaging (~> 10.12.0) - - firebase_core (2.15.1): - - Firebase/CoreOnly (= 10.12.0) + - FirebaseMessaging (~> 10.18.0) + - firebase_core (2.23.0): + - Firebase/CoreOnly (= 10.18.0) - Flutter - - firebase_messaging (14.6.6): - - Firebase/Messaging (= 10.12.0) + - firebase_messaging (14.7.5): + - Firebase/Messaging (= 10.18.0) - firebase_core - Flutter - - FirebaseCore (10.12.0): + - FirebaseCore (10.18.0): - FirebaseCoreInternal (~> 10.0) - - GoogleUtilities/Environment (~> 7.8) - - GoogleUtilities/Logger (~> 7.8) - - FirebaseCoreInternal (10.14.0): + - GoogleUtilities/Environment (~> 7.12) + - GoogleUtilities/Logger (~> 7.12) + - FirebaseCoreInternal (10.18.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseInstallations (10.14.0): + - FirebaseInstallations (10.18.0): - FirebaseCore (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) - PromisesObjC (~> 2.1) - - FirebaseMessaging (10.12.0): + - FirebaseMessaging (10.18.0): - FirebaseCore (~> 10.0) - FirebaseInstallations (~> 10.0) - GoogleDataTransport (~> 9.2) @@ -38,50 +38,52 @@ PODS: - Flutter - flutter_webrtc (0.9.36): - Flutter - - WebRTC-SDK (= 114.5735.02) + - WebRTC-SDK (= 114.5735.08) - GoogleDataTransport (9.2.5): - GoogleUtilities/Environment (~> 7.7) - nanopb (< 2.30910.0, >= 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/AppDelegateSwizzler (7.11.5): + - GoogleUtilities/AppDelegateSwizzler (7.12.0): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - - GoogleUtilities/Environment (7.11.5): + - GoogleUtilities/Environment (7.12.0): - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.11.5): + - GoogleUtilities/Logger (7.12.0): - GoogleUtilities/Environment - - GoogleUtilities/Network (7.11.5): + - GoogleUtilities/Network (7.12.0): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.11.5)" - - GoogleUtilities/Reachability (7.11.5): + - "GoogleUtilities/NSData+zlib (7.12.0)" + - GoogleUtilities/Reachability (7.12.0): - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (7.11.5): + - GoogleUtilities/UserDefaults (7.12.0): - GoogleUtilities/Logger - - nanopb (2.30909.0): - - nanopb/decode (= 2.30909.0) - - nanopb/encode (= 2.30909.0) - - nanopb/decode (2.30909.0) - - nanopb/encode (2.30909.0) + - nanopb (2.30909.1): + - nanopb/decode (= 2.30909.1) + - nanopb/encode (= 2.30909.1) + - nanopb/decode (2.30909.1) + - nanopb/encode (2.30909.1) - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - permission_handler_apple (9.1.1): + - Flutter - PromisesObjC (2.3.1) - url_launcher_ios (0.0.1): - Flutter - - WebRTC-SDK (114.5735.02) + - WebRTC-SDK (114.5735.08) DEPENDENCIES: - audio_session (from `.symlinks/plugins/audio_session/ios`) - - Firebase/Messaging - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - Flutter (from `Flutter`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) SPEC REPOS: @@ -112,29 +114,32 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_webrtc/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: audio_session: 4f3e461722055d21515cf3261b64c973c062f345 - Firebase: 07150e75d142fb9399f6777fa56a187b17f833a0 - firebase_core: 4a3246a02f828a01c74a2c26427037786d90f17f - firebase_messaging: 13b378c8449cae7ec96c79570170943dd73d4738 - FirebaseCore: f86a1394906b97ac445ae49c92552a9425831bed - FirebaseCoreInternal: d558159ee6cc4b823c2296ecc193de9f6d9a5bb3 - FirebaseInstallations: f672b1eda64e6381c21d424a2f680a943fd83f3b - FirebaseMessaging: bb2c4f6422a753038fe137d90ae7c1af57251316 + Firebase: 414ad272f8d02dfbf12662a9d43f4bba9bec2a06 + firebase_core: 29d66baf806970cda37c93621b27cd369b27db1b + firebase_messaging: 0a39f2514e1f27b0274b0d2fa99048f57856ee7c + FirebaseCore: 2322423314d92f946219c8791674d2f3345b598f + FirebaseCoreInternal: 8eb002e564b533bdcf1ba011f33f2b5c10e2ed4a + FirebaseInstallations: e842042ec6ac1fd2e37d7706363ebe7f662afea4 + FirebaseMessaging: 9bc34a98d2e0237e1b121915120d4d48ddcf301e Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 - flutter_webrtc: 1944895d4e908c4bc722929dc4b9f8620d8e1b2f + flutter_webrtc: 55df3aaa802114dad390191a46c2c8d535751268 GoogleDataTransport: 54dee9d48d14580407f8f5fbf2f496e92437a2f2 - GoogleUtilities: 13e2c67ede716b8741c7989e26893d151b2b2084 - nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 + GoogleUtilities: 0759d1a57ebb953965c2dfe0ba4c82e95ccc2e34 + nanopb: d4d75c12cd1316f4a64e3c6963f879ecd4b5e0d5 path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 - url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 - WebRTC-SDK: dd913fd31cfbf1d43b9a22d83f4c6354c960c623 + url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b + WebRTC-SDK: c24d2a6c9f571f2ed42297cb8ffba9557093142b -PODFILE CHECKSUM: 4795d042abc5acd4532a44cf526446489e113bab +PODFILE CHECKSUM: cc1f88378b4bfcf93a6ce00d2c587857c6008d3b -COCOAPODS: 1.12.0 +COCOAPODS: 1.14.3 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 243ec3d..520635b 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -8,12 +8,9 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 2AFE261E29EC785400B3D853 /* NotificationService.m in Sources */ = {isa = PBXBuildFile; fileRef = 2AFE261D29EC785400B3D853 /* NotificationService.m */; }; - 2AFE262229EC785400B3D853 /* ImageNotification.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2AFE261A29EC785400B3D853 /* ImageNotification.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 2AFE262C29EC7A7100B3D853 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2AFE262B29EC7A7100B3D853 /* GoogleService-Info.plist */; }; 2AFE263029EC7C3C00B3D853 /* ringtone.wav in Resources */ = {isa = PBXBuildFile; fileRef = 2AFE262F29EC7C3C00B3D853 /* ringtone.wav */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 5ACDD787E3C657C92DBC134C /* Pods_ImageNotification.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B54523D2DDC3F2A5828AB77 /* Pods_ImageNotification.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -21,16 +18,6 @@ FD91F7BD6865A876168AA93E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D3A9FC6A332A3FA53EE4E976 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ -/* Begin PBXContainerItemProxy section */ - 2AFE262029EC785400B3D853 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 2AFE261929EC785400B3D853; - remoteInfo = ImageNotification; - }; -/* End PBXContainerItemProxy section */ - /* Begin PBXCopyFilesBuildPhase section */ 2AFE261129EC773F00B3D853 /* Embed Foundation Extensions */ = { isa = PBXCopyFilesBuildPhase; @@ -38,7 +25,6 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( - 2AFE262229EC785400B3D853 /* ImageNotification.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -61,7 +47,6 @@ 1D3A49B4068FA52AA8D90F9A /* Pods-ImageNotification.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ImageNotification.profile.xcconfig"; path = "Target Support Files/Pods-ImageNotification/Pods-ImageNotification.profile.xcconfig"; sourceTree = ""; }; 2AFE260B29EC773F00B3D853 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 2AFE260D29EC773F00B3D853 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 2AFE261A29EC785400B3D853 /* ImageNotification.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ImageNotification.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 2AFE261C29EC785400B3D853 /* NotificationService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NotificationService.h; sourceTree = ""; }; 2AFE261D29EC785400B3D853 /* NotificationService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NotificationService.m; sourceTree = ""; }; 2AFE261F29EC785400B3D853 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -91,14 +76,6 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 2AFE261729EC785400B3D853 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 5ACDD787E3C657C92DBC134C /* Pods_ImageNotification.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -160,7 +137,6 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, - 2AFE261A29EC785400B3D853 /* ImageNotification.appex */, ); name = Products; sourceTree = ""; @@ -207,24 +183,6 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 2AFE261929EC785400B3D853 /* ImageNotification */ = { - isa = PBXNativeTarget; - buildConfigurationList = 2AFE262329EC785400B3D853 /* Build configuration list for PBXNativeTarget "ImageNotification" */; - buildPhases = ( - A6FC08691D38778947F34927 /* [CP] Check Pods Manifest.lock */, - 2AFE261629EC785400B3D853 /* Sources */, - 2AFE261729EC785400B3D853 /* Frameworks */, - 2AFE261829EC785400B3D853 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = ImageNotification; - productName = ImageNotification; - productReference = 2AFE261A29EC785400B3D853 /* ImageNotification.appex */; - productType = "com.apple.product-type.app-extension"; - }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; @@ -242,7 +200,6 @@ buildRules = ( ); dependencies = ( - 2AFE262129EC785400B3D853 /* PBXTargetDependency */, ); name = Runner; productName = Runner; @@ -256,12 +213,9 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1430; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { - 2AFE261929EC785400B3D853 = { - CreatedOnToolsVersion = 14.3; - }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; @@ -282,19 +236,11 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, - 2AFE261929EC785400B3D853 /* ImageNotification */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 2AFE261829EC785400B3D853 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -381,39 +327,9 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - A6FC08691D38778947F34927 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-ImageNotification-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 2AFE261629EC785400B3D853 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 2AFE261E29EC785400B3D853 /* NotificationService.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -425,14 +341,6 @@ }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXTargetDependency section */ - 2AFE262129EC785400B3D853 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 2AFE261929EC785400B3D853 /* ImageNotification */; - targetProxy = 2AFE262029EC785400B3D853 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -507,7 +415,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; @@ -530,115 +438,6 @@ }; name = Profile; }; - 2AFE262429EC785400B3D853 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 2AFE262D29EC7AEF00B3D853 /* DebugNotification.xcconfig */; - buildSettings = { - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 3QLZPMLJ3W; - GCC_C_LANGUAGE_STANDARD = gnu11; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = ImageNotification/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = ImageNotification; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - MARKETING_VERSION = 1.0; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.dieklingel.app.ImageNotification; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 2AFE262529EC785400B3D853 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 2AFE262D29EC7AEF00B3D853 /* DebugNotification.xcconfig */; - buildSettings = { - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 3QLZPMLJ3W; - GCC_C_LANGUAGE_STANDARD = gnu11; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = ImageNotification/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = ImageNotification; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - MARKETING_VERSION = 1.0; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.dieklingel.app.ImageNotification; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.dieklingel.app.ImageNotification"; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - 2AFE262629EC785400B3D853 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 2AFE262E29EC7AEF00B3D853 /* ReleaseNotification.xcconfig */; - buildSettings = { - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 3QLZPMLJ3W; - GCC_C_LANGUAGE_STANDARD = gnu11; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = ImageNotification/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = ImageNotification; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - MARKETING_VERSION = 1.0; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.dieklingel.app.ImageNotification; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Profile; - }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -750,7 +549,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; @@ -778,7 +577,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; @@ -806,16 +605,6 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 2AFE262329EC785400B3D853 /* Build configuration list for PBXNativeTarget "ImageNotification" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 2AFE262429EC785400B3D853 /* Debug */, - 2AFE262529EC785400B3D853 /* Release */, - 2AFE262629EC785400B3D853 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c87d15a..a6b826d 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ { - final HomeRepository homeRepository; - final IceServerRepository iceServerRepository; - RtcClientWrapper? rtcclient; - CancelableOperation? _requestOperation; - MqttClient? client; - Subscription? candidateSub; - - CallViewBloc( - this.homeRepository, - this.iceServerRepository, - ) : super(CallState()) { - on(_onStart); - on(_onHangup); - on(_onToogleMicrophone); - on(_onToogleSpeaker); - } - - HiveHome get home { - HiveHome? home = homeRepository.selected; - if (home == null) { - throw Exception( - "cannot read the selected home. Please select a Home in HomeRepository first", - ); - } - return home; - } - - Future _onStart(CallStart event, Emitter emit) async { - emit(CallInitatedState()); - - final MqttClient client = MqttClient(home.uri); - try { - await client.connect( - username: home.username ?? "", - password: home.password ?? "", - ); - } on mqtt.NoConnectionException catch (exception) { - emit(CallCancelState(exception.toString())); - return; - } - - String uuid = const Uuid().v4(); - - // create rtc connection - rtcclient = await RtcClientWrapper.create( - uuid: uuid, - iceServers: iceServerRepository.servers, - transceivers: [ - RtcTransceiver( - kind: RTCRtpMediaType.RTCRtpMediaTypeAudio, - direction: TransceiverDirection.SendRecv, - ), - RtcTransceiver( - kind: RTCRtpMediaType.RTCRtpMediaTypeVideo, - direction: TransceiverDirection.SendRecv, - ) - ], - ); - - candidateSub?.cancel(); - candidateSub = client.subscribe( - path.normalize("./$uuid/connection/candidate"), - (topic, message) { - Map candidate = jsonDecode( - Request.fromMap( - jsonDecode(message), - ).body, - ); - - rtcclient?.addIceCandidate( - RTCIceCandidate( - candidate["candidate"], - candidate["sdpMid"], - candidate["sdpMLineIndex"] as int, - ), - ); - }, - ); - - rtcclient!.onIceCandidate((RTCIceCandidate candidate) { - client.publish( - path.normalize("./${home.uri.path}/rtc/connections/candidate/$uuid"), - Request.withJsonBody("GET", candidate.toMap()).toJsonString(), - ); - }); - - await rtcclient!.ressource.open(true, false); - MediaStream? stream = rtcclient!.ressource.stream; - if (null != stream) { - for (MediaStreamTrack track in stream.getTracks()) { - rtcclient!.connection.addTrack(track, stream); - // Helper.setMicrophoneMute(true, track); - } - } - - if (rtcclient == null) { - return; - } - - await _requestOperation?.cancel(); - - String answerChannel = const Uuid().v4(); - final operation = CancelableOperation.fromFuture( - client - .once( - path.normalize( - "./${home.uri.path}/rtc/connections/create/$uuid/$answerChannel", - ), - timeout: const Duration(seconds: 15), - ) - .catchError((error) => ""), - ); - client.publish( - path.normalize("./${home.uri.path}/rtc/connections/create/$uuid"), - Request.withJsonBody( - "GET", - (await rtcclient!.offer()).toMap(), - ).withAnswerChannel(answerChannel).toJsonString(), - ); - _requestOperation = operation; - - await operation.value.then((value) async { - if (value.isEmpty) { - emit(CallCancelState("the doorunit did not send a repsonse")); - return; - } - - Response response = Response.fromMap(jsonDecode(value)); - if (response.statusCode != 201) { - add(CallHangup()); - return; - } - - final Map answer = jsonDecode(response.body); - final description = RTCSessionDescription( - answer["sdp"], - answer["type"], - ); - await rtcclient!.setRemoteDescription(description); - - RTCVideoRenderer? renderer = rtcclient?.renderer; - if (renderer == null) { - return; - } - - if (state is! CallInitatedState) { - add(CallHangup()); - return; - } - - emit( - CallActiveState( - microphoneState: rtcclient!.microphoneState, - speakerState: rtcclient!.speakerState, - renderer: renderer, - ), - ); - }).onError((exception, stackTrace) async { - emit(CallCancelState( - "the doorunit did not respond to the message", - )); - rtcclient?.ressource.close(); - await rtcclient?.dispose(); - rtcclient = null; - }); - } - - Future _onHangup(CallHangup event, Emitter emit) async { - String? uuid = rtcclient?.uuid; - _requestOperation?.cancel(); - - emit(CallEndedState()); - rtcclient?.ressource.close(); - await rtcclient?.dispose(); - rtcclient = null; - - client?.publish( - path.normalize("./${home.uri.path}/rtc/connections/close/$uuid"), - jsonEncode( - Request("GET", ""), - ), - ); - candidateSub?.cancel(); - candidateSub = null; - client?.disconnect(); - client = null; - } - - Future _onToogleMicrophone( - CallToogleMicrophone event, - Emitter emit, - ) async { - if (rtcclient == null) { - return; - } - - rtcclient!.microphoneState = rtcclient!.microphoneState.next(); - - RTCVideoRenderer? renderer = rtcclient?.renderer; - if (renderer == null) { - emit(CallEndedState()); - return; - } - - emit( - CallActiveState( - microphoneState: rtcclient!.microphoneState, - speakerState: rtcclient!.speakerState, - renderer: renderer, - ), - ); - } - - Future _onToogleSpeaker( - CallToogleSpeaker event, - Emitter emit, - ) async { - if (rtcclient == null) { - return; - } - - rtcclient!.speakerState = rtcclient!.speakerState.next( - skip: [if (kIsWeb) SpeakerState.headphone], - ); - - RTCVideoRenderer? renderer = rtcclient?.renderer; - if (renderer == null) { - emit(CallEndedState()); - return; - } - - emit( - CallActiveState( - microphoneState: rtcclient!.microphoneState, - speakerState: rtcclient!.speakerState, - renderer: renderer, - ), - ); - } - - @override - Future close() async { - candidateSub?.cancel(); - client?.disconnect(); - client = null; - await rtcclient?.dispose(); - return super.close(); - } -} diff --git a/lib/blocs/home_add_view_bloc.dart b/lib/blocs/home_add_view_bloc.dart deleted file mode 100644 index f44a731..0000000 --- a/lib/blocs/home_add_view_bloc.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'package:dieklingel_app/models/device.dart'; -import 'package:dieklingel_app/models/hive_home.dart'; -import 'package:dieklingel_app/models/request.dart'; -import 'package:dieklingel_app/repositories/home_repository.dart'; -import 'package:dieklingel_app/states/home_add_state.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:mqtt/mqtt.dart'; -import 'package:path/path.dart' as path; - -class HomeAddViewBloc extends Bloc { - final HomeRepository homeRepository; - - HomeAddViewBloc(this.homeRepository) : super(HomeAddState()) { - on(_onSubmit); - } - - Future _onSubmit( - HomeAddSubmit event, - Emitter emit, - ) async { - String? nameError; - if (event.name.isEmpty) { - nameError = "Please enter a name"; - } - - String? serverError; - RegExp serverRegex = RegExp( - r'^(mqtt|mqtts|ws|wss):\/\/(?:[A-Za-z0-9]+\.)+[A-Za-z0-9]{2,3}:\d{1,5}(\/?)$', - ); - if (!serverRegex.hasMatch(event.server)) { - serverError = - "Please enter a server url within the format 'mqtt://server.org:1883/'"; - } - - String? channelError; - RegExp channelRegex = RegExp( - r'^\/?(([a-z])+([a-z.])+([a-z])+(\/?))+$', - ); - if (!channelRegex.hasMatch(event.channel)) { - channelError = - "Please enter a channel prefix within format 'com.dieklingel/main/prefix/'"; - } - - String? signError; - RegExp signRegex = RegExp( - r'^[A-Za-z]+$', - ); - if (!signRegex.hasMatch(event.sign)) { - signError = "Please enter a sign within the format 'mysign'"; - } - - HomeAddFormErrorState errorState = HomeAddFormErrorState( - nameError: nameError, - serverError: serverError, - channelError: channelError, - signError: signError, - ); - if (errorState.hasError) { - emit(errorState); - return; - } - - Uri url = Uri.parse(event.server); - Uri uri = Uri.parse( - "${url.scheme}://${url.authority}/${event.channel}#${event.sign}", - ); - - HiveHome home = event.home ?? HiveHome(name: event.name, uri: uri); - home.name = event.name; - home.uri = uri; - home.username = event.username; - home.password = event.password; - - emit(HomeAddLoadingState()); - final client = MqttClient(home.uri); - try { - await client.connect( - username: home.username ?? "", - password: home.password ?? "", - ); - } catch (e) { - emit( - HomeAddErrorState( - "Could not conenct the the server ${uri.scheme}://${uri.host}:${uri.port} because ${e.toString()}", - ), - ); - return; - } - - await homeRepository.add(home); - await homeRepository.select(home); - emit(HomeAddSuccessfulState()); - - Box settingsBox = Hive.box("settings"); - String? token = settingsBox.get("token"); - - if (token == null) { - return; - } - - await client.publish( - path.normalize("./${home.uri.path}/devices/save"), - Request.withJsonBody( - "GET", - Device( - token, - signs: [home.uri.fragment], - ).toMap(), - ).toJsonString(), - ); - client.disconnect(); - } -} diff --git a/lib/blocs/home_list_view_bloc.dart b/lib/blocs/home_list_view_bloc.dart deleted file mode 100644 index c27d849..0000000 --- a/lib/blocs/home_list_view_bloc.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:dieklingel_app/states/home_list_state.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../repositories/home_repository.dart'; - -class HomeListViewBloc extends Bloc { - final HomeRepository homeRepository; - - HomeListViewBloc(this.homeRepository) : super(HomeListState()) { - on(_onDeleted); - on(_onRefresh); - - add(HomeListRefresh()); - } - - Future _onDeleted( - HomeListDeleted event, - Emitter emit, - ) async { - await homeRepository.delete(event.home); - emit(HomeListState(homes: homeRepository.homes)); - } - - Future _onRefresh( - HomeListRefresh event, - Emitter emit, - ) async { - emit(HomeListState(homes: homeRepository.homes)); - } -} diff --git a/lib/blocs/home_view_bloc.dart b/lib/blocs/home_view_bloc.dart deleted file mode 100644 index e2ffe06..0000000 --- a/lib/blocs/home_view_bloc.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'dart:async'; - -import 'package:dieklingel_app/models/hive_home.dart'; -import 'package:dieklingel_app/repositories/home_repository.dart'; -import 'package:dieklingel_app/states/home_state.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class HomeViewBloc extends Bloc { - final HomeRepository homeRepository; - - HomeViewBloc(this.homeRepository) : super(HomeState()) { - on(_onSelected); - on(_onRefresh); - - add(HomeRefresh()); - } - - Future _onSelected(HomeSelected event, Emitter emit) async { - await homeRepository.select(event.home); - emit( - HomeSelectedState(home: event.home, homes: homeRepository.homes), - ); - } - - Future _onRefresh(HomeRefresh event, Emitter emit) async { - HiveHome? selected = homeRepository.selected; - if (selected == null && homeRepository.homes.isNotEmpty) { - await homeRepository.select(homeRepository.homes.first); - selected = homeRepository.selected; - } - - if (selected == null) { - emit(HomeState( - homes: homeRepository.homes, - )); - } else { - emit( - HomeSelectedState(home: selected, homes: homeRepository.homes), - ); - } - } -} diff --git a/lib/components/fade_page_route.dart b/lib/components/fade_page_route.dart new file mode 100644 index 0000000..382a195 --- /dev/null +++ b/lib/components/fade_page_route.dart @@ -0,0 +1,26 @@ +import 'package:flutter/cupertino.dart'; + +class FadePageRoute extends PageRouteBuilder { + final Widget Function(BuildContext) builder; + + FadePageRoute({required this.builder}) + : super( + transitionDuration: const Duration(milliseconds: 150), + reverseTransitionDuration: const Duration(milliseconds: 150), + pageBuilder: (context, animation, secAnimaton) { + return builder(context); + }, + transitionsBuilder: (context, animation, secAnimation, child) { + final tween = Tween(begin: 0.0, end: 1.0); + final curvedAnimation = CurvedAnimation( + parent: animation, + curve: Curves.ease, + ); + + return FadeTransition( + opacity: tween.animate(curvedAnimation), + child: child, + ); + }, + ); +} diff --git a/lib/components/icon_builder.dart b/lib/components/icon_builder.dart deleted file mode 100644 index 2b00595..0000000 --- a/lib/components/icon_builder.dart +++ /dev/null @@ -1,127 +0,0 @@ -import 'package:dieklingel_app/components/map_builder.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; - -class IconBuilder extends MapBuilder { - IconBuilder({super.id, super.fallback, super.values}); - - @override - IconBuilder withValues(Map map) { - return IconBuilder(id: super.id, values: map, fallback: super.fallback); - } - - @override - IconBuilder withFallback(Icon fallback) { - return IconBuilder(id: super.id, values: super.values, fallback: fallback); - } - - @override - IconBuilder from(T_ID? id) { - return IconBuilder(id: id, values: super.values, fallback: super.fallback); - } - - @override - Icon build({ - IconData? icon, - Key? key, - double? size, - double? fill, - double? weight, - double? grade, - double? opticalSize, - Color? color, - List? shadows, - String? semanticLabel, - TextDirection? textDirection, - }) { - Icon build = super.build(); - - return Icon( - icon ?? build.icon, - key: key ?? build.key, - size: size ?? build.size, - fill: fill ?? build.fill, - weight: weight ?? build.weight, - grade: grade ?? build.grade, - opticalSize: opticalSize ?? build.opticalSize, - color: color ?? build.color, - shadows: shadows ?? build.shadows, - semanticLabel: semanticLabel ?? build.semanticLabel, - textDirection: textDirection ?? build.textDirection, - ); - } -} - -/* class IconBuilder { - final T_ID? _id; - final Map _values; - final T_ICON? _fallback; - - IconBuilder() - : _id = null, - _fallback = null, - _values = {}; - - IconBuilder._(this._id, this._values, this._fallback); - - IconBuilder values(Map map) { - return IconBuilder._(_id, map, _fallback); - } - - IconBuilder fallback(T_ICON fallback) { - return IconBuilder._(_id, _values, fallback); - } - - IconBuilder from(T_ID? id) { - return IconBuilder._(id, _values, _fallback); - } - - Icon build({ - IconData? iconData, - Key? key, - double? size, - double? fill, - double? weight, - double? grade, - double? opticalSize, - Color? color, - List? shadows, - String? semanticLabel, - TextDirection? textDirection, - }) { - if (_id == null) { - throw BuilderException( - "Cannot build an Icon without a source; Call Builder().from(id).", - ); - } - - Icon? icon = _values[_id]; - if (icon == null) { - Icon? fallback = _fallback; - - if (fallback == null) { - throw BuilderException( - "The given source was not found in values, and no fallback was set.", - ); - } - - icon = fallback; - } - - return Icon( - iconData ?? icon.icon, - key: key ?? icon.key, - size: size ?? icon.size, - fill: fill ?? icon.fill, - weight: weight ?? icon.weight, - grade: grade ?? icon.grade, - opticalSize: opticalSize ?? icon.opticalSize, - color: color ?? icon.color, - shadows: shadows ?? icon.shadows, - semanticLabel: semanticLabel ?? icon.semanticLabel, - textDirection: textDirection ?? icon.textDirection, - ); - } -}*/ - - diff --git a/lib/components/map_builder.dart b/lib/components/map_builder.dart deleted file mode 100644 index a2d0040..0000000 --- a/lib/components/map_builder.dart +++ /dev/null @@ -1,52 +0,0 @@ -class MapBuilder { - final T_ID? id; - final Map values; - final T_VALUE? fallback; - - MapBuilder({this.id, this.fallback, this.values = const {}}); - - MapBuilder withValues(Map map) { - return MapBuilder(id: id, values: map, fallback: fallback); - } - - MapBuilder withFallback(T_VALUE fallback) { - return MapBuilder(id: id, values: values, fallback: fallback); - } - - MapBuilder from(T_ID? id) { - return MapBuilder(id: id, values: values, fallback: fallback); - } - - T_VALUE build() { - if (id == null && fallback == null) { - throw BuilderException("cannot build without id and fallback"); - } - - T_VALUE? value = values[id]; - - if (value == null) { - T_VALUE? fallback = this.fallback; - - if (fallback == null) { - throw BuilderException( - "The given source was not found in values, and no fallback was set.", - ); - } - - value = fallback; - } - - return value; - } -} - -class BuilderException implements Exception { - final String message; - - BuilderException(this.message); - - @override - String toString() { - return message; - } -} diff --git a/lib/components/stream_subscription_mixin.dart b/lib/components/stream_subscription_mixin.dart new file mode 100644 index 0000000..8199200 --- /dev/null +++ b/lib/components/stream_subscription_mixin.dart @@ -0,0 +1,21 @@ +import 'dart:async'; + +class StreamHandler { + final List _subscriptions = []; + + void subscribe(Stream stream, void Function(T) handler) { + _subscriptions.add(stream.listen(handler)); + } + + Future dispose() async { + await Future.wait( + _subscriptions.map( + (sub) => sub.cancel(), + ), + ); + } +} + +mixin StreamHandlerMixin { + final StreamHandler streams = StreamHandler(); +} diff --git a/lib/event/system_event.dart b/lib/event/system_event.dart deleted file mode 100644 index 82e1fda..0000000 --- a/lib/event/system_event.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'dart:convert'; - -import 'system_event_type.dart'; - -class SystemEvent { - final DateTime timestamp; - final SystemEventType type; - final String payload; - - SystemEvent({ - required this.type, - required this.payload, - }) : timestamp = DateTime.now().toUtc(); - - SystemEvent.fromJson(Map json) - : timestamp = DateTime.parse(json["timestamp"]), - type = SystemEventType.fromString(json["type"]), - payload = json["payload"]; - - Map toJson() => { - 'timestamp': timestamp.toIso8601String(), - 'type': type.toString(), - 'payload': payload, - }; - - @override - String toString() { - return jsonEncode(toJson()); - } -} diff --git a/lib/event/system_event_type.dart b/lib/event/system_event_type.dart deleted file mode 100644 index b018848..0000000 --- a/lib/event/system_event_type.dart +++ /dev/null @@ -1,23 +0,0 @@ -enum SystemEventType { - image("image"), - text("text"), - notification("notification"); - - final String type; - const SystemEventType(this.type); - - static SystemEventType fromString(String value) { - switch (value) { - case "image": - return SystemEventType.image; - case "notification": - return SystemEventType.notification; - } - return SystemEventType.text; - } - - @override - String toString() { - return type; - } -} diff --git a/lib/extensions/uri.dart b/lib/extensions/uri.dart deleted file mode 100644 index f7c56d6..0000000 --- a/lib/extensions/uri.dart +++ /dev/null @@ -1,10 +0,0 @@ -extension BetterUri on Uri { - Uri append({String? path}) { - return replace( - pathSegments: [ - ...pathSegments, - ...((path ?? "").split("/")..removeWhere((element) => element.isEmpty)), - ], - ).normalizePath(); - } -} diff --git a/lib/handlers/call_handler.dart b/lib/handlers/call_handler.dart deleted file mode 100644 index 68881b4..0000000 --- a/lib/handlers/call_handler.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:shelf/shelf.dart'; -import 'package:shelf_router/shelf_router.dart'; - -class CallHandler { - final Uri uri; - final String uuid; - final String? username; - final String? password; - final Router handler = Router(); - - CallHandler( - this.uri, { - required this.uuid, - this.username, - this.password, - }) { - handler.connect("/rtc/connections/$uuid", (Request request) {}); - } -} diff --git a/lib/hive/hive_home_adapter.dart b/lib/hive/hive_home_adapter.dart index 8b1fea2..046d0d1 100644 --- a/lib/hive/hive_home_adapter.dart +++ b/lib/hive/hive_home_adapter.dart @@ -1,14 +1,12 @@ import 'package:dieklingel_app/models/home.dart'; import 'package:hive_flutter/hive_flutter.dart'; -import '../models/hive_home.dart'; - -class HiveHomeAdapter extends TypeAdapter { +class HiveHomeAdapter extends TypeAdapter { @override - HiveHome read(BinaryReader reader) { + Home read(BinaryReader reader) { Map map = reader.readMap().cast(); - HiveHome home = HiveHome.fromMap(map); + Home home = Home.fromMap(map); return home; } diff --git a/lib/main.dart b/lib/main.dart index dc9dcf1..a1b4146 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,15 +1,13 @@ -import 'package:dieklingel_app/blocs/call_view_bloc.dart'; -import 'package:dieklingel_app/blocs/home_add_view_bloc.dart'; -import 'package:dieklingel_app/blocs/home_view_bloc.dart'; +import 'package:dieklingel_app/ui/home/home_view_model.dart'; import 'package:dieklingel_app/handlers/notification_handler.dart'; -import 'package:dieklingel_app/models/device.dart'; -import 'package:dieklingel_app/models/request.dart'; + import 'package:dieklingel_app/repositories/home_repository.dart'; import 'package:dieklingel_app/repositories/ice_server_repository.dart'; -import 'package:dieklingel_app/views/home_view.dart'; +import 'package:dieklingel_app/ui/home/home_view.dart'; +import 'package:dieklingel_app/ui/settings/homes/homes_view_model.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:mqtt/mqtt.dart'; -import 'package:path/path.dart' as path; + +import 'package:provider/provider.dart'; import './models/home.dart'; import 'package:hive_flutter/hive_flutter.dart'; @@ -23,7 +21,6 @@ import 'blocs/ice_server_add_view_bloc.dart'; import 'firebase_options.dart'; import 'hive/hive_home_adapter.dart'; import 'hive/hive_ice_server_adapter.dart'; -import 'models/hive_home.dart'; import 'models/hive_ice_server.dart'; import 'models/ice_server.dart'; @@ -36,7 +33,7 @@ void main() async { ..registerAdapter(HiveIceServerAdapter()); await Future.wait([ - Hive.openBox((Home).toString()), + Hive.openBox((Home).toString()), Hive.openBox((IceServer).toString()), Hive.openBox("settings"), ]); @@ -53,15 +50,11 @@ void main() async { RepositoryProvider(create: (_) => homeRepository), RepositoryProvider(create: (_) => iceServerRepository), ], - child: MultiBlocProvider( + child: MultiProvider( providers: [ - BlocProvider(create: (_) => HomeViewBloc(homeRepository)), - BlocProvider(create: (_) => HomeAddViewBloc(homeRepository)), BlocProvider( create: (_) => IceServerAddViewBloc(iceServerRepository)), - BlocProvider( - create: (_) => CallViewBloc(homeRepository, iceServerRepository), - ), + ChangeNotifierProvider(create: (_) => HomesViewModel(homeRepository)) ], child: const App(), ), @@ -70,7 +63,7 @@ void main() async { } class App extends StatefulWidget { - const App({Key? key}) : super(key: key); + const App({super.key}); @override State createState() => _App(); @@ -106,26 +99,6 @@ class _App extends State { Box settingsBox = Hive.box("settings"); settingsBox.put("token", token); - - Box box = Hive.box((Home).toString()); - for (HiveHome home in box.values) { - final client = MqttClient(home.uri); - await client.connect( - username: home.username ?? "", - password: home.password ?? "", - ); - await client.publish( - path.normalize("./${home.uri.path}/devices/save"), - Request.withJsonBody( - "GET", - Device( - token, - signs: [home.uri.fragment], - ).toMap(), - ).toJsonString(), - ); - client.disconnect(); - } } void initialize() { @@ -139,8 +112,8 @@ class _App extends State { @override Widget build(BuildContext context) { return CupertinoApp( - home: BlocProvider( - create: (_) => HomeViewBloc(context.read()), + home: ChangeNotifierProvider( + create: (_) => HomeViewModel(context.read()), child: const HomeView(), ), ); diff --git a/lib/models/audio/microphone_state.dart b/lib/models/audio/microphone_state.dart new file mode 100644 index 0000000..e1a49cb --- /dev/null +++ b/lib/models/audio/microphone_state.dart @@ -0,0 +1,4 @@ +enum MicrophoneState { + muted, + unmuted, +} diff --git a/lib/models/audio/speaker_state.dart b/lib/models/audio/speaker_state.dart new file mode 100644 index 0000000..3572952 --- /dev/null +++ b/lib/models/audio/speaker_state.dart @@ -0,0 +1,5 @@ +enum SpeakerState { + muted, + earphone, + speaker, +} diff --git a/lib/models/call/call.dart b/lib/models/call/call.dart new file mode 100644 index 0000000..15227d1 --- /dev/null +++ b/lib/models/call/call.dart @@ -0,0 +1,211 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:async/async.dart'; +import 'package:dieklingel_app/components/stream_subscription_mixin.dart'; +import 'package:flutter/foundation.dart'; + +import 'package:flutter/material.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; + +import '../../utils/media_ressource.dart'; +import '../audio/microphone_state.dart'; +import '../audio/speaker_state.dart'; +import '../ice_server.dart'; + +class Call with StreamHandlerMixin { + final String id; + final List iceServers; + final renderer = RTCVideoRenderer(); + final _localIceCandidates = StreamController(); + final _remoteIceCandidates = StreamController(); + final _connectionState = StreamController(); + final _media = MediaRessource(); + late final _remoteIceCandidatesBuffer = + _remoteIceCandidates.stream.listenAndBuffer(); + late final _localIceCandidatesBuffer = + _localIceCandidates.stream.listenAndBuffer(); + + RTCPeerConnection? connection; + SpeakerState _speaker = SpeakerState.muted; + MicrophoneState _microphone = MicrophoneState.muted; + + Call( + this.id, + this.iceServers, + ) { + WidgetsFlutterBinding.ensureInitialized(); + if (!kIsWeb && Platform.isIOS) { + Helper.setAppleAudioIOMode(AppleAudioIOMode.localAndRemote); + } + } + + List get loaclAudioTracks { + final conn = connection; + if (conn == null) { + return []; + } + + final List tracks = conn + .getLocalStreams() + .whereType() + .expand( + (stream) => stream.getAudioTracks(), + ) + .toList(); + return tracks; + } + + List get remoteAudioTracks { + final conn = connection; + if (conn == null) { + return []; + } + + final List tracks = conn + .getRemoteStreams() + .whereType() + .expand( + (stream) => stream.getAudioTracks(), + ) + .toList(); + return tracks; + } + + SpeakerState get speaker { + return _speaker; + } + + set speaker(SpeakerState state) { + _speaker = state; + for (final track in remoteAudioTracks) { + switch (_speaker) { + case SpeakerState.muted: + track.enabled = false; + break; + case SpeakerState.earphone: + track.enabled = true; + if (!kIsWeb) { + track.enableSpeakerphone(false); + } + break; + case SpeakerState.speaker: + track.enabled = true; + if (!kIsWeb) { + track.enableSpeakerphone(true); + } + break; + } + } + } + + MicrophoneState get microphone { + return _microphone; + } + + set microphone(MicrophoneState state) { + _microphone = state; + for (final track in loaclAudioTracks) { + switch (_microphone) { + case MicrophoneState.muted: + track.enabled = false; + break; + case MicrophoneState.unmuted: + track.enabled = true; + break; + } + } + } + + Future offer() async { + await renderer.initialize(); + + connection = await createPeerConnection({ + "iceServers": iceServers.map((e) => e.toMap()).toList(), + "sdpSemantics": "unified-plan", + }); + + connection! + ..onIceCandidate = (candidate) { + _localIceCandidates.add(candidate); + } + ..onTrack = (event) { + if (event.track.kind == "audio") { + switch (_speaker) { + case SpeakerState.muted: + event.track.enabled = false; + break; + case SpeakerState.earphone: + event.track.enabled = true; + if (!kIsWeb) { + event.track.enableSpeakerphone(false); + } + break; + case SpeakerState.speaker: + event.track.enabled = true; + if (!kIsWeb) { + event.track.enableSpeakerphone(true); + } + break; + } + } + renderer.srcObject = event.streams.first; + } + ..onConnectionState = (state) { + _connectionState.add(state); + } + ..onIceConnectionState = (state) { + if (state != RTCIceConnectionState.RTCIceConnectionStateConnected) { + return; + } + streams.subscribe(_remoteIceCandidatesBuffer, (candidate) { + connection!.addCandidate(candidate); + }); + }; + + final stream = await _media.open(true, false); + + await connection!.addTransceiver( + kind: RTCRtpMediaType.RTCRtpMediaTypeAudio, + init: RTCRtpTransceiverInit( + direction: TransceiverDirection.SendRecv, streams: [stream!]), + ); + await connection!.addTransceiver( + kind: RTCRtpMediaType.RTCRtpMediaTypeVideo, + init: RTCRtpTransceiverInit(direction: TransceiverDirection.RecvOnly), + ); + + final offer = await connection!.createOffer(); + await connection!.setLocalDescription(offer); + return offer; + } + + Future withRemoteAnswer(RTCSessionDescription answer) async { + await connection!.setRemoteDescription(answer); + } + + RTCPeerConnectionState get state { + final connectionState = connection?.connectionState; + if (connectionState == null) { + return RTCPeerConnectionState.RTCPeerConnectionStateClosed; + } + + return connectionState; + } + + Stream get localIceCandidates { + return _localIceCandidatesBuffer; + } + + Sink get remoteIceCandidates { + return _remoteIceCandidates.sink; + } + + Future close() async { + streams.dispose(); + _media.close(); + await _localIceCandidates.close(); + await _remoteIceCandidates.close(); + await renderer.dispose(); + } +} diff --git a/lib/models/device.dart b/lib/models/device.dart deleted file mode 100644 index 18dd096..0000000 --- a/lib/models/device.dart +++ /dev/null @@ -1,13 +0,0 @@ -class Device { - final String token; - final List signs; - - Device(this.token, {this.signs = const []}); - - Map toMap() { - return { - "token": token, - "signs": signs, - }; - } -} diff --git a/lib/models/hive_home.dart b/lib/models/hive_home.dart deleted file mode 100644 index b4d5cfa..0000000 --- a/lib/models/hive_home.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:dieklingel_app/models/home.dart'; -import 'package:hive_flutter/hive_flutter.dart'; - -class HiveHome extends Home with HiveObjectMixin { - HiveHome({ - required super.name, - required super.uri, - super.username, - super.password, - }); - - factory HiveHome.fromMap(Map map) { - if (!map.containsKey("name")) { - throw "Cannot create Home from Map without name"; - } - if (!map.containsKey("uri")) { - throw "Cannot create Home from Map without uri"; - } - - return HiveHome( - name: map["name"], - uri: Uri.parse(map["uri"]), - username: map["username"], - password: map["password"], - ); - } - - @override - Future save() async { - if (isInBox) { - await super.save(); - return; - } - Box box = Hive.box((Home).toString()); - await box.add(this); - } -} diff --git a/lib/models/home.dart b/lib/models/home.dart index 89a0738..b417df8 100644 --- a/lib/models/home.dart +++ b/lib/models/home.dart @@ -1,15 +1,21 @@ +import 'package:uuid/uuid.dart'; + class Home { - String name; - Uri uri; - String? username; - String? password; + final String id; + final String name; + final Uri uri; + final String? username; + final String? password; + final String? passcode; Home({ + String? id, required this.name, required this.uri, this.username, this.password, - }); + this.passcode, + }) : id = (id == null || id.isEmpty) ? const Uuid().v4() : id; factory Home.fromMap(Map map) { if (!map.containsKey("name")) { @@ -20,19 +26,23 @@ class Home { } return Home( + id: map["id"], name: map["name"], uri: Uri.parse(map["uri"]), username: map["username"], password: map["password"], + passcode: map["passcode"], ); } Map toMap() { return { + "id": id, "name": name, "uri": uri.toString(), "username": username, "password": password, + "passcode": passcode, }; } @@ -45,12 +55,15 @@ class Home { Uri? uri, String? username, String? password, + String? passcode, }) => Home( + id: id, name: name ?? this.name, uri: uri ?? this.uri, username: username ?? this.username, - password: this.password, + password: password ?? this.password, + passcode: passcode ?? this.passcode, ); @override @@ -58,14 +71,16 @@ class Home { if (other is! Home) { return false; } - return name == other.name && + return id == other.id && + name == other.name && uri == other.uri && username == other.username && - password == other.password; + password == other.password && + passcode == other.passcode; } @override - int get hashCode => Object.hash(name, uri, username, password); + int get hashCode => Object.hash(id, name, uri, username, password, passcode); @override String toString() { diff --git a/lib/models/messages/answer_message.dart b/lib/models/messages/answer_message.dart new file mode 100644 index 0000000..74a2f1c --- /dev/null +++ b/lib/models/messages/answer_message.dart @@ -0,0 +1,37 @@ +import 'package:blueprint/blueprint.dart'; +import 'package:dieklingel_app/models/messages/session_message_header.dart'; + +import 'session_message_body.dart'; + +class AnswerMessage { + final SessionMessageHeader header; + final SessionMessageBody body; + + AnswerMessage({ + required this.header, + required this.body, + }); + + factory AnswerMessage.fromMap(dynamic map) { + matchMap( + map, + { + "header": MapF, + "body": MapF, + }, + throwable: true, + ); + + return AnswerMessage( + header: SessionMessageHeader.fromMap(map["header"]), + body: SessionMessageBody.fromMap(map["body"]), + ); + } + + Map toMap() { + return { + "header": header.toMap(), + "body": body.toMap(), + }; + } +} diff --git a/lib/models/messages/candidate_message.dart b/lib/models/messages/candidate_message.dart new file mode 100644 index 0000000..525aec4 --- /dev/null +++ b/lib/models/messages/candidate_message.dart @@ -0,0 +1,36 @@ +import 'package:blueprint/blueprint.dart'; +import 'package:dieklingel_app/models/messages/candidate_message_body.dart'; +import 'package:dieklingel_app/models/messages/session_message_header.dart'; + +class CandidateMessage { + final SessionMessageHeader header; + final CandidateMessageBody body; + + CandidateMessage({ + required this.header, + required this.body, + }); + + factory CandidateMessage.fromMap(dynamic map) { + matchMap( + map, + { + "header": MapF, + "body": MapF, + }, + throwable: true, + ); + + return CandidateMessage( + header: SessionMessageHeader.fromMap(map["header"]), + body: CandidateMessageBody.fromMap(map["body"]), + ); + } + + Map toMap() { + return { + "header": header.toMap(), + "body": body.toMap(), + }; + } +} diff --git a/lib/models/messages/candidate_message_body.dart b/lib/models/messages/candidate_message_body.dart new file mode 100644 index 0000000..c97eb1a --- /dev/null +++ b/lib/models/messages/candidate_message_body.dart @@ -0,0 +1,36 @@ +import 'package:blueprint/blueprint.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; + +class CandidateMessageBody { + final RTCIceCandidate iceCandidate; + + CandidateMessageBody({required this.iceCandidate}); + + factory CandidateMessageBody.fromMap(dynamic map) { + matchMap( + map, + { + "iceCandidate": MapF.of({ + "candidate": StringF, + "sdpMid": StringF, + "sdpMLineIndex": IntF, + }), + }, + throwable: true, + ); + + return CandidateMessageBody( + iceCandidate: RTCIceCandidate( + map["iceCandidate"]["candidate"], + map["iceCandidate"]["sdpMid"], + map["iceCandidate"]["sdpMLineIndex"], + ), + ); + } + + Map toMap() { + return { + "iceCandidate": iceCandidate.toMap(), + }; + } +} diff --git a/lib/models/messages/close_message.dart b/lib/models/messages/close_message.dart new file mode 100644 index 0000000..1785104 --- /dev/null +++ b/lib/models/messages/close_message.dart @@ -0,0 +1,31 @@ +import 'package:blueprint/blueprint.dart'; + +import 'session_message_header.dart'; + +class CloseMessage { + final SessionMessageHeader header; + + CloseMessage({ + required this.header, + }); + + factory CloseMessage.fromMap(dynamic map) { + matchMap( + map, + { + "header": MapF, + }, + throwable: true, + ); + + return CloseMessage( + header: SessionMessageHeader.fromMap(map["header"]), + ); + } + + Map toMap() { + return { + "header": header.toMap(), + }; + } +} diff --git a/lib/models/messages/message_header.dart b/lib/models/messages/message_header.dart new file mode 100644 index 0000000..cb8cea3 --- /dev/null +++ b/lib/models/messages/message_header.dart @@ -0,0 +1,34 @@ +import 'package:blueprint/blueprint.dart'; + +class MessageHeader { + final String senderDeviceId; + final String senderSessionId; + + MessageHeader({ + required this.senderDeviceId, + required this.senderSessionId, + }); + + factory MessageHeader.fromMap(dynamic map) { + matchMap( + map, + { + "senderDeviceId": StringF, + "senderSessionId": StringF, + }, + throwable: true, + ); + + return MessageHeader( + senderDeviceId: map["senderDeviceId"], + senderSessionId: map["senderSessionId"], + ); + } + + Map toMap() { + return { + "senderDeviceId": senderDeviceId, + "senderSessionId": senderSessionId, + }; + } +} diff --git a/lib/models/messages/offer_message.dart b/lib/models/messages/offer_message.dart new file mode 100644 index 0000000..a2d7353 --- /dev/null +++ b/lib/models/messages/offer_message.dart @@ -0,0 +1,37 @@ +import 'package:blueprint/blueprint.dart'; + +import 'message_header.dart'; +import 'session_message_body.dart'; + +class OfferMessage { + final MessageHeader header; + final SessionMessageBody body; + + OfferMessage({ + required this.header, + required this.body, + }); + + factory OfferMessage.fromMap(dynamic map) { + matchMap( + map, + { + "header": MapF, + "body": MapF, + }, + throwable: true, + ); + + return OfferMessage( + header: MessageHeader.fromMap(map["header"]), + body: SessionMessageBody.fromMap(map["body"]), + ); + } + + Map toMap() { + return { + "header": header.toMap(), + "body": body.toMap(), + }; + } +} diff --git a/lib/models/messages/session_message_body.dart b/lib/models/messages/session_message_body.dart new file mode 100644 index 0000000..3d07958 --- /dev/null +++ b/lib/models/messages/session_message_body.dart @@ -0,0 +1,36 @@ +import 'package:blueprint/blueprint.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; + +class SessionMessageBody { + final RTCSessionDescription sessionDescription; + + SessionMessageBody({ + required this.sessionDescription, + }); + + factory SessionMessageBody.fromMap(Map map) { + matchMap( + map, + { + "sessionDescription": MapF.of({ + "type": StringF, + "sdp": StringF, + }) + }, + throwable: true, + ); + + return SessionMessageBody( + sessionDescription: RTCSessionDescription( + map["sessionDescription"]["sdp"], + map["sessionDescription"]["type"], + ), + ); + } + + Map toMap() { + return { + "sessionDescription": sessionDescription.toMap(), + }; + } +} diff --git a/lib/models/messages/session_message_header.dart b/lib/models/messages/session_message_header.dart new file mode 100644 index 0000000..adf8810 --- /dev/null +++ b/lib/models/messages/session_message_header.dart @@ -0,0 +1,39 @@ +import 'package:blueprint/blueprint.dart'; + +class SessionMessageHeader { + final String senderDeviceId; + final String sessionId; + final String senderSessionId; + + SessionMessageHeader({ + required this.senderDeviceId, + required this.sessionId, + required this.senderSessionId, + }); + + factory SessionMessageHeader.fromMap(dynamic map) { + matchMap( + map, + { + "senderDeviceId": StringF, + "sessionId": StringF, + "senderSessionId": StringF, + }, + throwable: true, + ); + + return SessionMessageHeader( + senderDeviceId: map["senderDeviceId"], + sessionId: map["sessionId"], + senderSessionId: map["senderSessionId"], + ); + } + + Map toMap() { + return { + "senderDeviceId": senderDeviceId, + "sessionId": sessionId, + "senderSessionId": senderSessionId, + }; + } +} diff --git a/lib/models/request.dart b/lib/models/request.dart deleted file mode 100644 index e6c814e..0000000 --- a/lib/models/request.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'dart:convert'; - -class Request { - final String body; - final String method; - final Map headers; - - Request(this.method, this.body, {this.headers = const {}}); - - factory Request.fromMap(Map map) { - String body = map["body"]; - String method = map["method"]; - Map headers = map["headers"]; - - return Request(method, body, headers: headers.cast()); - } - - factory Request.withJsonBody( - String method, - Map body, { - Map headers = const {}, - }) { - String json = jsonEncode(body); - return Request(method, json, headers: headers); - } - - Request withAnswerChannel(String topic) { - Map header = {}; - header.addAll(headers); - header["mqtt_answer_channel"] = topic; - - return Request(method, body, headers: header); - } - - Map toMap() { - return { - "method": method, - "body": body, - "headers": headers, - }; - } - - String toJsonString() { - return jsonEncode(toMap()); - } -} diff --git a/lib/models/response.dart b/lib/models/response.dart deleted file mode 100644 index 4ee34d1..0000000 --- a/lib/models/response.dart +++ /dev/null @@ -1,27 +0,0 @@ -class Response { - final String body; - final Map headers; - final int statusCode; - - Response( - this.statusCode, - this.body, { - this.headers = const {}, - }); - - factory Response.fromMap(Map map) { - String body = map["body"]; - Map headers = map["headers"]; - int statusCode = map["statusCode"]; - - return Response(statusCode, body, headers: headers.cast()); - } - - Map toMap() { - return { - "statusCode": statusCode, - "body": body, - "headers": headers, - }; - } -} diff --git a/lib/repositories/home_repository.dart b/lib/repositories/home_repository.dart index 8bcc4e1..91b20ca 100644 --- a/lib/repositories/home_repository.dart +++ b/lib/repositories/home_repository.dart @@ -1,58 +1,32 @@ -import 'package:dieklingel_app/models/hive_home.dart'; +import 'dart:async'; + import 'package:hive_flutter/hive_flutter.dart'; import '../models/home.dart'; class HomeRepository { - final Box _homebox = Hive.box((Home).toString()); - final Box _settingsbox = Hive.box("settings"); - - List get homes => _homebox.values.toList(); - - HiveHome? get selected { - dynamic key = _settingsbox.get("home"); - if (key == null) { - return null; - } - if (!_homebox.containsKey(key) && _homebox.isNotEmpty) { - select(_homebox.values.first); - return _homebox.values.first; - } - return _homebox.get(key); - } + final Box _homebox = Hive.box((Home).toString()); + final _add = StreamController.broadcast(); + final _remove = StreamController.broadcast(); + final _change = StreamController.broadcast(); - Future add(HiveHome home) async { - if (home.isInBox) { - await home.save(); - return; - } - await _homebox.add(home); - } + List get homes => _homebox.values.toList(); + Stream get added => _add.stream; + Stream get changed => _change.stream; + Stream get removed => _remove.stream; - Future delete(HiveHome home) async { - if (!home.isInBox) { - return; - } - await home.delete(); - if (homes.isEmpty) { - await select(null); - return; - } - if (selected == home) { - select(homes.first); + Future add(Home home) async { + final exists = _homebox.containsKey(home.id); + await _homebox.put(home.id, home); + if (exists) { + _change.add(home); + } else { + _add.add(home); } } - Future select(HiveHome? home) async { - if (home == null) { - await _settingsbox.delete("home"); - return; - } - if (!homes.contains(home)) { - throw Exception( - "The selected home cannot be selected, because it is not saved!", - ); - } - await _settingsbox.put("home", home.key); + Future delete(Home home) async { + await _homebox.delete(home.id); + _remove.add(home); } } diff --git a/lib/signaling/signaling_message.dart b/lib/signaling/signaling_message.dart deleted file mode 100644 index 1808d9a..0000000 --- a/lib/signaling/signaling_message.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'dart:convert'; - -import './signaling_message_type.dart'; - -class SignalingMessage { - SignalingMessageType type = SignalingMessageType.error; - Map data = {}; - - SignalingMessage(); - - SignalingMessage.fromJson(Map json) - : type = SignalingMessageType.fromString(json['type']), - data = json['data']; - - Map toJson() => { - 'type': type.toString(), - 'data': data, - }; - - String toJsonString() { - return jsonEncode(toJson()); - } - - @override - String toString() { - return jsonEncode(toJson()); - } -} diff --git a/lib/signaling/signaling_message_type.dart b/lib/signaling/signaling_message_type.dart deleted file mode 100644 index 15100c8..0000000 --- a/lib/signaling/signaling_message_type.dart +++ /dev/null @@ -1,33 +0,0 @@ -enum SignalingMessageType { - offer("offer"), - answer("answer"), - candidate("new-ice-candidate"), - leave("leave"), - busy("busy"), - error("error"); - - static SignalingMessageType fromString(String value) { - switch (value) { - case "offer": - return SignalingMessageType.offer; - case "answer": - return SignalingMessageType.answer; - case "new-ice-candidate": - return SignalingMessageType.candidate; - case "leave": - return SignalingMessageType.leave; - case "busy": - return SignalingMessageType.busy; - default: - return SignalingMessageType.error; - } - } - - const SignalingMessageType(this.value); - final String value; - - @override - String toString() { - return value; - } -} diff --git a/lib/states/call_state.dart b/lib/states/call_state.dart deleted file mode 100644 index b1f0d8a..0000000 --- a/lib/states/call_state.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:dieklingel_app/utils/microphone_state.dart'; -import 'package:dieklingel_app/utils/speaker_state.dart'; -import 'package:flutter_webrtc/flutter_webrtc.dart'; - -class CallState {} - -class CallErrorState extends CallState { - final String errorMessage; - - CallErrorState({required this.errorMessage}); -} - -class CallInitatedState extends CallState {} - -class CallActiveState extends CallState { - final MicrophoneState microphoneState; - final SpeakerState speakerState; - final RTCVideoRenderer renderer; - - CallActiveState({ - required this.microphoneState, - required this.speakerState, - required this.renderer, - }); -} - -class CallEndedState extends CallState {} - -class CallCancelState extends CallEndedState { - final String reason; - - CallCancelState(this.reason); -} - -abstract class CallEvent {} - -class CallStart extends CallEvent {} - -class CallHangup extends CallEvent {} - -class CallToogleMicrophone extends CallEvent {} - -class CallToogleSpeaker extends CallEvent {} diff --git a/lib/states/home_add_state.dart b/lib/states/home_add_state.dart deleted file mode 100644 index 3d28aaa..0000000 --- a/lib/states/home_add_state.dart +++ /dev/null @@ -1,122 +0,0 @@ -import 'package:dieklingel_app/models/hive_home.dart'; - -class HomeAddState {} - -class HomeAddInitialState extends HomeAddState { - final String name; - final String server; - final String username; - final String password; - final String channel; - final String sign; - - HomeAddInitialState({ - required this.name, - required this.server, - required this.username, - required this.password, - required this.channel, - required this.sign, - }); -} - -class HomeAddErrorState extends HomeAddState { - final String errorMessage; - - HomeAddErrorState(this.errorMessage); -} - -class HomeAddLoadingState extends HomeAddState {} - -class HomeAddFormErrorState extends HomeAddState { - final String? nameError; - final String? serverError; - final String? channelError; - final String? signError; - - bool get hasError { - return nameError != null || - serverError != null || - channelError != null || - signError != null; - } - - HomeAddFormErrorState({ - this.nameError, - this.serverError, - this.channelError, - this.signError, - }); -} - -class HomeAddSuccessfulState extends HomeAddState {} - -class HomeAddEvent {} - -class HomeAddInitialize extends HomeAddEvent { - final HiveHome? home; - - HomeAddInitialize({this.home}); -} - -class HomeAdd extends HomeAddEvent { - final HiveHome home; - - HomeAdd({required this.home}); -} - -class HomeAddName extends HomeAddEvent { - final String name; - - HomeAddName({required this.name}); -} - -class HomeAddServer extends HomeAddEvent { - final String server; - - HomeAddServer({required this.server}); -} - -class HomeAddUsername extends HomeAddEvent { - final String username; - - HomeAddUsername({required this.username}); -} - -class HomeAddPassword extends HomeAddEvent { - final String password; - - HomeAddPassword({required this.password}); -} - -class HomeAddChannel extends HomeAddEvent { - final String channel; - - HomeAddChannel({required this.channel}); -} - -class HomeAddSign extends HomeAddEvent { - final String sign; - - HomeAddSign({required this.sign}); -} - -class HomeAddSubmit extends HomeAddEvent { - final HiveHome? home; - final String name; - final String server; - final String username; - final String password; - final String channel; - final String sign; - - HomeAddSubmit({ - required this.name, - required this.server, - required this.username, - required this.password, - required this.channel, - required this.sign, - this.home, - }); -} diff --git a/lib/states/home_list_state.dart b/lib/states/home_list_state.dart deleted file mode 100644 index 8ae9847..0000000 --- a/lib/states/home_list_state.dart +++ /dev/null @@ -1,17 +0,0 @@ -import '../models/hive_home.dart'; - -class HomeListState { - final List homes; - - HomeListState({this.homes = const []}); -} - -class HomeListEvent {} - -class HomeListRefresh extends HomeListEvent {} - -class HomeListDeleted extends HomeListEvent { - final HiveHome home; - - HomeListDeleted({required this.home}); -} diff --git a/lib/states/home_state.dart b/lib/states/home_state.dart deleted file mode 100644 index 44047fb..0000000 --- a/lib/states/home_state.dart +++ /dev/null @@ -1,23 +0,0 @@ -import '../models/hive_home.dart'; - -class HomeState { - final List homes; - - HomeState({this.homes = const []}); -} - -class HomeSelectedState extends HomeState { - final HiveHome home; - - HomeSelectedState({required this.home, super.homes}); -} - -class HomeEvent {} - -class HomeSelected extends HomeEvent { - final HiveHome home; - - HomeSelected({required this.home}); -} - -class HomeRefresh extends HomeEvent {} diff --git a/lib/ui/call/active/call_active_view.dart b/lib/ui/call/active/call_active_view.dart new file mode 100644 index 0000000..93f8357 --- /dev/null +++ b/lib/ui/call/active/call_active_view.dart @@ -0,0 +1,74 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; +import 'package:provider/provider.dart'; + +import 'call_active_view_model.dart'; +import 'widgets/microphone_button.dart'; +import 'widgets/speaker_button.dart'; + +class CallActiveView extends StatefulWidget { + const CallActiveView({super.key}); + + @override + State createState() => _CallActiveViewState(); +} + +class _CallActiveViewState extends State { + @override + void initState() { + super.initState(); + context.read().onHangup().then((_) { + Navigator.pop(context); + }); + } + + @override + Widget build(BuildContext context) { + final renderer = context.select( + (CallActiveViewModel vm) => vm.renderer, + ); + + return CupertinoPageScaffold( + child: Stack( + children: [ + InteractiveViewer( + child: RTCVideoView(renderer), + ), + SafeArea( + child: Container( + padding: const EdgeInsets.all(12.0), + alignment: Alignment.bottomCenter, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + const MicrophoneButton(), + const SpeakerButton(), + CupertinoButton( + color: Colors.amber, + onPressed: null, + padding: EdgeInsets.zero, + borderRadius: BorderRadius.circular(999), + child: const Icon(CupertinoIcons.lock_fill), + ), + Hero( + tag: "call_hangup_button", + child: CupertinoButton( + color: Colors.red, + padding: EdgeInsets.zero, + borderRadius: BorderRadius.circular(999), + onPressed: () { + context.read().hangup(); + }, + child: const Icon(CupertinoIcons.xmark), + ), + ) + ], + ), + ), + ) + ], + ), + ); + } +} diff --git a/lib/ui/call/active/call_active_view_model.dart b/lib/ui/call/active/call_active_view_model.dart new file mode 100644 index 0000000..c4c5d66 --- /dev/null +++ b/lib/ui/call/active/call_active_view_model.dart @@ -0,0 +1,143 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; +import 'package:dieklingel_app/components/stream_subscription_mixin.dart'; +import 'package:dieklingel_app/models/audio/speaker_state.dart'; +import 'package:dieklingel_app/models/messages/candidate_message_body.dart'; +import 'package:dieklingel_app/models/messages/session_message_header.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; +import 'package:mqtt/mqtt.dart' as mqtt; +import 'package:path/path.dart'; +import '../../../models/audio/microphone_state.dart'; +import '../../../models/call/call.dart'; +import '../../../models/home.dart'; +import '../../../models/messages/candidate_message.dart'; +import '../../../models/messages/close_message.dart'; + +class CallActiveViewModel extends ChangeNotifier with StreamHandlerMixin { + final Home home; + final mqtt.Client connection; + final Call call; + final Completer _onHangup = Completer(); + final String remoteSessionId; + + CallActiveViewModel({ + required this.home, + required this.connection, + required this.call, + required this.remoteSessionId, + }) { + streams.subscribe( + connection.topic("${home.username}/connections/candidate"), + (event) { + final (_, message) = event; + try { + final payload = CandidateMessage.fromMap(json.decode(message)); + if (payload.header.sessionId != call.id) { + return; + } + + call.remoteIceCandidates.add(payload.body.iceCandidate); + } catch (e) { + log("could not parse the candidate message; message: $message, error: $e"); + } + }, + ); + + streams.subscribe(connection.topic("${home.username}/connections/close"), + (event) async { + final (_, message) = event; + + try { + final payload = CloseMessage.fromMap(json.decode(message)); + if (payload.header.sessionId != call.id) { + return; + } + + _onHangup.complete(); + await call.close(); + } catch (e) { + log("could not parse the close message; message: $message, error: $e"); + } + }); + + streams.subscribe(call.localIceCandidates, (candidate) { + final payload = CandidateMessage( + header: SessionMessageHeader( + senderDeviceId: home.username!, + sessionId: remoteSessionId, + senderSessionId: call.id, + ), + body: CandidateMessageBody(iceCandidate: candidate), + ); + + connection.publish( + normalize("./${home.uri.path}/connections/candidate"), + json.encode(payload.toMap()), + ); + }); + + call.renderer.addListener(() { + notifyListeners(); + }); + + call.renderer.onFirstFrameRendered = () { + // BUG: this method gets not called on web + log("the first frame of the video was renderd"); + notifyListeners(); + }; + } + + set microphone(MicrophoneState state) { + call.microphone = state; + notifyListeners(); + } + + MicrophoneState get microphone { + return call.microphone; + } + + set speaker(SpeakerState state) { + call.speaker = state; + notifyListeners(); + } + + SpeakerState get speaker { + return call.speaker; + } + + RTCVideoRenderer get renderer { + return call.renderer; + } + + Future onHangup() async { + return _onHangup.future; + } + + void hangup() { + streams.dispose(); + call.close(); + + final payload = CloseMessage( + header: SessionMessageHeader( + senderDeviceId: home.username ?? "", + senderSessionId: call.id, + sessionId: remoteSessionId, + ), + ); + + connection.publish( + normalize("./${home.uri.path}/connections/close"), + json.encode(payload.toMap()), + ); + _onHangup.complete(null); + } + + @override + void dispose() { + streams.dispose(); + super.dispose(); + } +} diff --git a/lib/ui/call/active/widgets/microphone_button.dart b/lib/ui/call/active/widgets/microphone_button.dart new file mode 100644 index 0000000..17c30db --- /dev/null +++ b/lib/ui/call/active/widgets/microphone_button.dart @@ -0,0 +1,63 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:pull_down_button/pull_down_button.dart'; + +import '../../../../models/audio/microphone_state.dart'; +import '../call_active_view_model.dart'; + +class MicrophoneButton extends StatelessWidget { + const MicrophoneButton({super.key}); + + @override + Widget build(BuildContext context) { + final microphone = context.select( + (CallActiveViewModel vm) => vm.microphone, + ); + + return PullDownButton( + itemBuilder: (context) { + return [ + PullDownMenuItem.selectable( + onTap: () { + final vm = context.read(); + vm.microphone = MicrophoneState.muted; + }, + title: "Muted", + icon: CupertinoIcons.mic_slash_fill, + selected: microphone == MicrophoneState.muted, + ), + PullDownMenuItem.selectable( + // TODO: enable microhone + enabled: false, + onTap: () { + final vm = context.read(); + vm.microphone = MicrophoneState.unmuted; + }, + title: "Unmuted", + icon: CupertinoIcons.mic_fill, + selected: microphone == MicrophoneState.unmuted, + ), + ]; + }, + buttonBuilder: (context, showMenu) { + return CupertinoButton( + color: Colors.lightBlue, + padding: EdgeInsets.zero, + borderRadius: BorderRadius.circular(999), + onPressed: showMenu, + child: Icon( + (() { + switch (microphone) { + case MicrophoneState.muted: + return CupertinoIcons.mic_slash_fill; + case MicrophoneState.unmuted: + return CupertinoIcons.mic_fill; + } + })(), + ), + ); + }, + ); + } +} diff --git a/lib/ui/call/active/widgets/speaker_button.dart b/lib/ui/call/active/widgets/speaker_button.dart new file mode 100644 index 0000000..d0298a1 --- /dev/null +++ b/lib/ui/call/active/widgets/speaker_button.dart @@ -0,0 +1,74 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:pull_down_button/pull_down_button.dart'; + +import '../../../../models/audio/speaker_state.dart'; +import '../call_active_view_model.dart'; + +class SpeakerButton extends StatelessWidget { + const SpeakerButton({super.key}); + + @override + Widget build(BuildContext context) { + final speaker = context.select( + (CallActiveViewModel vm) => vm.speaker, + ); + + return PullDownButton( + itemBuilder: (context) { + return [ + PullDownMenuItem.selectable( + onTap: () { + final vm = context.read(); + vm.speaker = SpeakerState.muted; + }, + title: "Muted", + icon: CupertinoIcons.speaker_slash_fill, + selected: speaker == SpeakerState.muted, + ), + if (!kIsWeb) + PullDownMenuItem.selectable( + onTap: () { + final vm = context.read(); + vm.speaker = SpeakerState.earphone; + }, + title: "Earphone", + icon: CupertinoIcons.ear, + selected: speaker == SpeakerState.earphone, + ), + PullDownMenuItem.selectable( + onTap: () { + final vm = context.read(); + vm.speaker = SpeakerState.speaker; + }, + title: "Speaker", + icon: CupertinoIcons.speaker_2_fill, + selected: speaker == SpeakerState.speaker, + ), + ]; + }, + buttonBuilder: (context, showMenu) { + return CupertinoButton( + color: Colors.lightGreen, + padding: EdgeInsets.zero, + borderRadius: BorderRadius.circular(999), + onPressed: showMenu, + child: Icon( + (() { + switch (speaker) { + case SpeakerState.muted: + return CupertinoIcons.speaker_slash_fill; + case SpeakerState.earphone: + return CupertinoIcons.ear; + case SpeakerState.speaker: + return CupertinoIcons.speaker_2_fill; + } + })(), + ), + ); + }, + ); + } +} diff --git a/lib/ui/call/outgoing/call_outgoing_view.dart b/lib/ui/call/outgoing/call_outgoing_view.dart new file mode 100644 index 0000000..b955edf --- /dev/null +++ b/lib/ui/call/outgoing/call_outgoing_view.dart @@ -0,0 +1,126 @@ +import 'dart:ui'; + +import 'package:dieklingel_app/ui/call/active/call_active_view_model.dart'; +import 'package:dieklingel_app/ui/call/outgoing/call_outgoing_view_model.dart'; +import 'package:dieklingel_app/ui/call/active/call_active_view.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:mqtt/mqtt.dart'; +import 'package:provider/provider.dart'; + +import '../../../components/fade_page_route.dart'; +import '../../../models/home.dart'; + +class CallOutgoingView extends StatefulWidget { + const CallOutgoingView({super.key}); + + @override + State createState() => _CallOutgoingViewState(); +} + +class _CallOutgoingViewState extends State { + @override + void initState() { + super.initState(); + + final Home home = context.read().home; + final Client connection = context.read().connection; + + context.read().onAnswer().then( + (event) { + final (call, remoteSessionId) = event; + + Navigator.pushReplacement( + context, + FadePageRoute( + builder: (context) { + return ChangeNotifierProvider( + create: (context) => CallActiveViewModel( + home: home, + connection: connection, + call: call, + remoteSessionId: remoteSessionId, + ), + child: const CallActiveView(), + ); + }, + ), + ); + }, + ); + + context.read().onHangup().then( + (_) { + Navigator.pop(context); + }, + ); + + context.read().call(); + } + + @override + Widget build(BuildContext context) { + final callee = context.select( + (value) => value.home.name, + ); + + return CupertinoPageScaffold( + backgroundColor: CupertinoColors.lightBackgroundGray, + child: Stack( + clipBehavior: Clip.none, + children: [ + Positioned.fill( + child: ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Image.asset( + "assets/images/house.png", + fit: BoxFit.cover, + color: Colors.grey, + colorBlendMode: BlendMode.darken, + ), + ), + ), + SafeArea( + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(56), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + children: [ + Text( + callee, + style: Theme.of(context).textTheme.headlineSmall, + ), + Text( + "outgoing call...", + style: Theme.of(context).textTheme.titleSmall, + ), + ], + ), + Hero( + tag: "call_hangup_button", + child: CupertinoButton( + color: Colors.red, + padding: EdgeInsets.zero, + minSize: kMinInteractiveDimensionCupertino * 1.2, + borderRadius: BorderRadius.circular(999), + child: const Icon( + CupertinoIcons.xmark, + ), + onPressed: () { + context.read().hangup(); + }, + ), + ) + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/call/outgoing/call_outgoing_view_model.dart b/lib/ui/call/outgoing/call_outgoing_view_model.dart new file mode 100644 index 0000000..983c8c9 --- /dev/null +++ b/lib/ui/call/outgoing/call_outgoing_view_model.dart @@ -0,0 +1,133 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; + +import 'package:dieklingel_app/components/stream_subscription_mixin.dart'; +import 'package:dieklingel_app/repositories/ice_server_repository.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:mqtt/mqtt.dart' as mqtt; +import 'package:path/path.dart'; +import 'package:uuid/uuid.dart'; + +import '../../../models/call/call.dart'; +import '../../../models/home.dart'; +import '../../../models/messages/answer_message.dart'; +import '../../../models/messages/candidate_message.dart'; +import '../../../models/messages/close_message.dart'; +import '../../../models/messages/message_header.dart'; +import '../../../models/messages/offer_message.dart'; +import '../../../models/messages/session_message_body.dart'; + +class CallOutgoingViewModel extends ChangeNotifier with StreamHandlerMixin { + final IceServerRepository iceServerRepository; + final Home home; + final mqtt.Client connection; + final Completer _onHangup = Completer(); + final Completer<(Call, String)> _onAnswer = Completer(); + Timer? _timeout; + + late final Call _call = Call(const Uuid().v4(), iceServerRepository.servers); + + CallOutgoingViewModel({ + required this.home, + required this.connection, + required this.iceServerRepository, + }) { + streams.subscribe( + connection.topic("${home.username}/connections/answer"), + (event) async { + final (_, message) = event; + try { + final payload = AnswerMessage.fromMap(json.decode(message)); + if (payload.header.sessionId != _call.id) { + return; + } + + _timeout?.cancel(); + await _call.withRemoteAnswer(payload.body.sessionDescription); + await streams.dispose(); + _onAnswer.complete((_call, payload.header.senderSessionId)); + } catch (e) { + log("could not parse the answer message; message: $message, error: $e"); + } + }, + ); + + streams.subscribe( + connection.topic("${home.username}/connections/candidate"), + (event) { + final (_, message) = event; + try { + final payload = CandidateMessage.fromMap(json.decode(message)); + if (payload.header.sessionId != _call.id) { + return; + } + + _call.remoteIceCandidates.add(payload.body.iceCandidate); + } catch (e) { + log("could not parse the candidate message; message: $message, error: $e"); + } + }, + ); + + streams.subscribe( + connection.topic("${home.username}/connections/close"), + (event) async { + final (_, message) = event; + try { + final payload = CloseMessage.fromMap(json.decode(message)); + if (payload.header.sessionId != _call.id) { + return; + } + + await _call.close(); + _onHangup.complete(null); + } catch (e) { + log("could not parse the close message; message: $message, error: $e"); + } + }, + ); + } + + Future onHangup() { + return _onHangup.future; + } + + Future<(Call, String)> onAnswer() async { + return _onAnswer.future; + } + + Future call() async { + if (_timeout != null) { + throw Exception("a call was already in progress"); + } + + final offer = await _call.offer(); + final payload = OfferMessage( + header: MessageHeader( + senderDeviceId: home.username ?? "", + senderSessionId: _call.id, + ), + body: SessionMessageBody( + sessionDescription: offer, + ), + ); + + connection.publish( + normalize("./${home.uri.path}/connections/offer"), + json.encode(payload.toMap()), + ); + _timeout = Timer(const Duration(seconds: 15), () { + if (_onHangup.isCompleted) { + return; + } + _onHangup.complete(); + }); + } + + void hangup() { + streams.dispose(); + _call.close(); + _onHangup.complete(); + } +} diff --git a/lib/ui/home/home_view.dart b/lib/ui/home/home_view.dart new file mode 100644 index 0000000..3af2a0f --- /dev/null +++ b/lib/ui/home/home_view.dart @@ -0,0 +1,109 @@ +import 'package:dieklingel_app/ui/home/widgets/app_bar_add.dart'; +import 'package:dieklingel_app/ui/home/widgets/app_bar_menu.dart'; +import 'package:dieklingel_app/ui/home/widgets/home_body.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:provider/provider.dart'; + +import '../../models/home.dart'; +import '../../repositories/home_repository.dart'; +import '../settings/homes/editor/home_editor_view.dart'; +import '../settings/homes/editor/home_editor_view_model.dart'; +import 'home_view_model.dart'; + +class HomeView extends StatefulWidget { + const HomeView({super.key}); + + @override + State createState() => _HomeViewState(); +} + +class _HomeViewState extends State { + Home? selected; + + @override + void initState() { + setState(() { + selected = context.read().homes.firstOrNull; + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final homes = context.select( + (HomeViewModel vm) => vm.homes, + ); + if (homes.isNotEmpty && !homes.contains(selected)) { + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + selected = homes.first; + }); + }); + } + + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: Text(selected?.name ?? "Homes"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const AppBarAdd(), + AppBarMenu( + homes: homes, + selected: selected, + onHomeTap: (home) { + setState(() { + selected = home; + }); + }, + onReconnectTap: (home) { + context.read().reconnect(home); + }, + ), + ], + ), + ), + child: Builder(builder: (context) { + if (homes.isEmpty) { + return Center( + child: CupertinoButton( + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(CupertinoIcons.add), + Text("add your first Home"), + ], + ), + onPressed: () { + showCupertinoModalPopup( + context: context, + builder: (context) { + return CupertinoPopupSurface( + child: ChangeNotifierProvider( + create: (_) => HomeEditorViewModel( + context.read(), + ), + child: const HomeEditorView(), + ), + ); + }, + ); + }, + ), + ); + } + + if (selected == null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + selected = homes.first; + }); + }); + return const CupertinoActivityIndicator(); + } + + return HomeBody(home: selected!); + }), + ); + } +} diff --git a/lib/ui/home/home_view_model.dart b/lib/ui/home/home_view_model.dart new file mode 100644 index 0000000..673492d --- /dev/null +++ b/lib/ui/home/home_view_model.dart @@ -0,0 +1,78 @@ +import 'package:dieklingel_app/models/home.dart'; +import 'package:mqtt/mqtt.dart' as mqtt; +import 'package:dieklingel_app/repositories/home_repository.dart'; +import 'package:flutter/cupertino.dart'; + +class HomeViewModel extends ChangeNotifier { + final HomeRepository homeRepository; + final Map _connections = {}; + + HomeViewModel(this.homeRepository) { + homeRepository.added.listen((home) { + final client = mqtt.Client(home.uri); + client.onConnectionStateChanged = (_) => notifyListeners(); + _connections[home.id] = client; + notifyListeners(); + + client.connect( + username: home.username ?? "", + password: home.password ?? "", + throws: false, + ); + }); + + homeRepository.changed.listen((home) { + _connections[home.id]?.disconnect(); + + final client = mqtt.Client(home.uri); + client.onConnectionStateChanged = (_) => notifyListeners(); + _connections[home.id] = client; + notifyListeners(); + + client.connect( + username: home.username ?? "", + password: home.password ?? "", + throws: false, + ); + }); + + homeRepository.removed.listen((home) { + _connections[home.id]?.disconnect(); + _connections.remove(home.id); + notifyListeners(); + }); + + for (final home in homeRepository.homes) { + final client = mqtt.Client(home.uri); + client.onConnectionStateChanged = (_) => notifyListeners(); + _connections[home.id] = client; + + client.connect( + username: home.username ?? "", + password: home.password ?? "", + throws: false, + ); + } + } + + List get homes { + return homeRepository.homes; + } + + mqtt.ConnectionState state(Home home) { + return _connections[home.id]!.state; + } + + void reconnect(Home home) { + _connections[home.id]?.disconnect(); + _connections[home.id]?.connect( + username: home.username ?? "", + password: home.password ?? "", + throws: false, + ); + } + + mqtt.Client client(Home home) { + return _connections[home.id]!; + } +} diff --git a/lib/ui/home/widgets/app_bar_add.dart b/lib/ui/home/widgets/app_bar_add.dart new file mode 100644 index 0000000..c47173c --- /dev/null +++ b/lib/ui/home/widgets/app_bar_add.dart @@ -0,0 +1,60 @@ +import 'package:flutter/cupertino.dart'; +import 'package:provider/provider.dart'; +import 'package:pull_down_button/pull_down_button.dart'; + +import '../../../repositories/home_repository.dart'; +import '../../../views/ice_server_add_view.dart'; +import '../../settings/homes/editor/home_editor_view.dart'; +import '../../settings/homes/editor/home_editor_view_model.dart'; + +class AppBarAdd extends StatelessWidget { + const AppBarAdd({super.key}); + + @override + Widget build(BuildContext context) { + return PullDownButton( + itemBuilder: (context) => [ + PullDownMenuItem( + onTap: () { + showCupertinoModalPopup( + context: context, + builder: (context) { + return CupertinoPopupSurface( + child: ChangeNotifierProvider( + create: (_) => HomeEditorViewModel( + context.read(), + ), + child: const HomeEditorView(), + ), + ); + }, + ); + }, + title: "add Home", + icon: CupertinoIcons.home, + ), + PullDownMenuItem( + onTap: () { + showCupertinoModalPopup( + context: context, + builder: (context) { + return const CupertinoPopupSurface( + child: IceServerAddView(), + ); + }, + ); + }, + title: "add ICE Server", + icon: CupertinoIcons.cloud, + ) + ], + buttonBuilder: (context, showMenu) { + return CupertinoButton( + padding: EdgeInsets.zero, + onPressed: showMenu, + child: const Icon(CupertinoIcons.plus), + ); + }, + ); + } +} diff --git a/lib/ui/home/widgets/app_bar_menu.dart b/lib/ui/home/widgets/app_bar_menu.dart new file mode 100644 index 0000000..3f7740e --- /dev/null +++ b/lib/ui/home/widgets/app_bar_menu.dart @@ -0,0 +1,59 @@ +import 'package:dieklingel_app/ui/settings/settings_view.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:pull_down_button/pull_down_button.dart'; + +import '../../../models/home.dart'; + +class AppBarMenu extends StatelessWidget { + final Home? selected; + final List homes; + final void Function(Home) onHomeTap; + final void Function(Home) onReconnectTap; + + const AppBarMenu({ + super.key, + required this.onHomeTap, + required this.onReconnectTap, + required this.homes, + required this.selected, + }); + + @override + Widget build(BuildContext context) { + return PullDownButton( + itemBuilder: (context) => [ + PullDownMenuItem( + onTap: () { + Navigator.of(context).push( + CupertinoPageRoute( + builder: (context) => const SettingsView(), + ), + ); + }, + title: "Settings", + icon: CupertinoIcons.settings, + ), + if (homes.isNotEmpty) const PullDownMenuDivider.large(), + for (final home in homes) ...[ + PullDownMenuItem.selectable( + onTap: () => onHomeTap(home), + title: home.name, + selected: home == selected, + ), + ], + if (selected != null) ...[const PullDownMenuDivider.large()], + if (selected != null) + PullDownMenuItem( + onTap: () => onReconnectTap(selected!), + title: "Reconnect", + icon: CupertinoIcons.refresh, + ), + ], + buttonBuilder: (context, showMenu) => CupertinoButton( + padding: EdgeInsets.zero, + onPressed: showMenu, + child: const Icon(CupertinoIcons.ellipsis_circle), + ), + ); + } +} diff --git a/lib/ui/home/widgets/home_body.dart b/lib/ui/home/widgets/home_body.dart new file mode 100644 index 0000000..b6e7c9a --- /dev/null +++ b/lib/ui/home/widgets/home_body.dart @@ -0,0 +1,95 @@ +import 'dart:ui'; + +import 'package:dieklingel_app/components/fade_page_route.dart'; +import 'package:dieklingel_app/repositories/ice_server_repository.dart'; +import 'package:dieklingel_app/ui/call/outgoing/call_outgoing_view.dart'; +import 'package:dieklingel_app/ui/call/outgoing/call_outgoing_view_model.dart'; +import 'package:dieklingel_app/ui/home/home_view_model.dart'; +import 'package:dieklingel_app/ui/home/widgets/home_connection_state.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:mqtt/mqtt.dart' as mqtt; +import 'package:provider/provider.dart'; + +import '../../../models/home.dart'; + +class HomeBody extends StatelessWidget { + final Home home; + + const HomeBody({ + super.key, + required this.home, + }); + + @override + Widget build(BuildContext context) { + final state = context.select((HomeViewModel vm) => vm.state(home)); + + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(12.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(10.0), + child: Stack( + children: [ + ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: 3, sigmaY: 3), + child: Image.asset( + "assets/images/house.png", + color: Colors.grey, + colorBlendMode: BlendMode.darken, + ), + ), + Positioned.fill( + child: CupertinoButton( + onPressed: state != mqtt.ConnectionState.connected + ? null + : () { + final connection = + context.read().client(home); + + Navigator.push( + context, + FadePageRoute( + builder: (_) { + return ChangeNotifierProvider( + create: (_) => CallOutgoingViewModel( + home: home, + connection: connection, + iceServerRepository: + context.read(), + ), + child: const CallOutgoingView(), + ); + }, + ), + ); + }, + child: const Icon( + CupertinoIcons.play_fill, + size: 45.0, + ), + ), + ) + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + HomeConnectionState(state), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/home/widgets/home_connection_state.dart b/lib/ui/home/widgets/home_connection_state.dart new file mode 100644 index 0000000..ea912ce --- /dev/null +++ b/lib/ui/home/widgets/home_connection_state.dart @@ -0,0 +1,45 @@ +import 'package:flutter/cupertino.dart'; +import 'package:mqtt/mqtt.dart' as mqtt; + +class HomeConnectionState extends StatelessWidget { + final mqtt.ConnectionState state; + + const HomeConnectionState(this.state, {super.key}); + + @override + Widget build(BuildContext context) { + final IconData icon; + final String message; + + switch (state) { + case mqtt.ConnectionState.connected: + icon = CupertinoIcons.check_mark_circled; + message = "Connected"; + break; + case mqtt.ConnectionState.connecting: + icon = CupertinoIcons.refresh_circled; + message = "Connecting..."; + break; + case mqtt.ConnectionState.disconnected: + icon = CupertinoIcons.xmark_circle; + message = "Disconnected"; + break; + case mqtt.ConnectionState.disconnecting: + icon = CupertinoIcons.xmark_circle; + message = "Disconnecting..."; + break; + case mqtt.ConnectionState.faulted: + icon = CupertinoIcons.xmark_circle; + message = "Connection-Error"; + break; + } + + return Row( + children: [ + Icon(icon), + const SizedBox(width: 8.0), + Text(message), + ], + ); + } +} diff --git a/lib/views/about_view.dart b/lib/ui/settings/about/about_view.dart similarity index 100% rename from lib/views/about_view.dart rename to lib/ui/settings/about/about_view.dart diff --git a/lib/ui/settings/homes/editor/home_editor_view.dart b/lib/ui/settings/homes/editor/home_editor_view.dart new file mode 100644 index 0000000..bac90e7 --- /dev/null +++ b/lib/ui/settings/homes/editor/home_editor_view.dart @@ -0,0 +1,115 @@ +import 'package:flutter/cupertino.dart'; +import 'package:provider/provider.dart'; + +import 'home_editor_view_model.dart'; + +class HomeEditorView extends StatefulWidget { + const HomeEditorView({super.key}); + + @override + State createState() => _HomeEditorView(); +} + +class _HomeEditorView extends State { + @override + Widget build(BuildContext context) { + final name = context.select((HomeEditorViewModel vm) => vm.name); + final server = context.select((HomeEditorViewModel vm) => vm.server); + final username = context.select((HomeEditorViewModel vm) => vm.username); + final password = context.select((HomeEditorViewModel vm) => vm.password); + final channel = context.select((HomeEditorViewModel vm) => vm.channel); + + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + leading: CupertinoButton( + padding: EdgeInsets.zero, + child: const Text("Cancel"), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + middle: const Text("Home"), + trailing: CupertinoButton( + padding: EdgeInsets.zero, + onPressed: () async { + final vm = context.read(); + await vm.save(); + if (!mounted) { + return; + } + Navigator.pop(context); + }, + child: const Text("Save"), + ), + ), + backgroundColor: CupertinoColors.systemGroupedBackground, + child: SafeArea( + child: ListView( + clipBehavior: Clip.none, + children: [ + CupertinoFormSection.insetGrouped( + header: const Text("Configuration"), + children: [ + CupertinoTextFormFieldRow( + prefix: const Text("Name"), + initialValue: name, + onChanged: (value) { + context.read().name = value; + }, + ), + ], + ), + CupertinoFormSection.insetGrouped( + header: const Text("Server"), + children: [ + CupertinoTextFormFieldRow( + prefix: const Text("Server URL"), + initialValue: server, + onChanged: (value) { + context.read().server = value; + }, + ), + CupertinoTextFormFieldRow( + prefix: const Text("Username"), + initialValue: username, + onChanged: (value) { + context.read().username = value; + }, + ), + CupertinoTextFormFieldRow( + prefix: const Text("Password"), + initialValue: password, + obscureText: true, + onChanged: (value) { + context.read().password = value; + }, + ), + ], + ), + CupertinoFormSection.insetGrouped( + header: const Text("Channel"), + children: [ + CupertinoTextFormFieldRow( + prefix: const Text("Channel Prefix"), + initialValue: channel, + onChanged: (value) { + context.read().channel = value; + }, + ), + ], + ), + CupertinoFormSection.insetGrouped( + header: const Text("Doorunit"), + children: [ + CupertinoTextFormFieldRow( + prefix: const Text("Passcode"), + obscureText: true, + ), + ], + ) + ], + ), + ), + ); + } +} diff --git a/lib/ui/settings/homes/editor/home_editor_view_model.dart b/lib/ui/settings/homes/editor/home_editor_view_model.dart new file mode 100644 index 0000000..faa1a4f --- /dev/null +++ b/lib/ui/settings/homes/editor/home_editor_view_model.dart @@ -0,0 +1,87 @@ +import 'package:dieklingel_app/models/home.dart'; +import 'package:dieklingel_app/repositories/home_repository.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:path/path.dart'; +import 'package:uuid/uuid.dart'; + +class HomeEditorViewModel extends ChangeNotifier { + final HomeRepository homeRepository; + late final String _id; + String _name; + String _server; + String _username; + String _password; + String _channel; + bool _isLoading = false; + + HomeEditorViewModel(this.homeRepository, {Home? home}) + : _id = home?.id ?? const Uuid().v4(), + _name = home?.name ?? "", + _server = home == null + ? "" + : "${home.uri.scheme}://${home.uri.host}:${home.uri.port}", + _username = home?.username ?? "", + _password = home?.password ?? "", + _channel = home == null ? "" : normalize("./${home.uri.path}"); + + bool get isLoading { + return _isLoading; + } + + set name(String value) { + _name = value; + notifyListeners(); + } + + String get name => _name; + + set server(String value) { + _server = value; + notifyListeners(); + } + + String get server => _server; + + set username(String value) { + _username = value; + notifyListeners(); + } + + String get username => _username; + + set password(String value) { + _password = value; + notifyListeners(); + } + + String get password => _password; + + set channel(String value) { + _channel = value; + notifyListeners(); + } + + String get channel => _channel; + + Future save() async { + _isLoading = true; + notifyListeners(); + + Uri url = Uri.parse(_server); + Uri uri = Uri.parse("${url.scheme}://${url.authority}/$_channel"); + + // TODO: check connection + await homeRepository.add( + Home( + id: _id, + name: name, + uri: uri, + username: _username, + password: _password, + ), + ); + + _isLoading = false; + notifyListeners(); + } +} diff --git a/lib/ui/settings/homes/homes_view.dart b/lib/ui/settings/homes/homes_view.dart new file mode 100644 index 0000000..d4c035e --- /dev/null +++ b/lib/ui/settings/homes/homes_view.dart @@ -0,0 +1,90 @@ +import 'package:dieklingel_app/repositories/home_repository.dart'; +import 'package:dieklingel_app/ui/settings/homes/editor/home_editor_view.dart'; +import 'package:dieklingel_app/ui/settings/homes/editor/home_editor_view_model.dart'; +import 'package:dieklingel_app/ui/settings/homes/homes_view_model.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:provider/provider.dart'; + +import '../../../models/home.dart'; +import 'widgets/list_entry.dart'; + +class HomesView extends StatelessWidget { + const HomesView({super.key}); + + void _onEditHome(BuildContext context, [Home? home]) async { + await Navigator.push( + context, + CupertinoModalPopupRoute( + builder: (context) => CupertinoPopupSurface( + child: ChangeNotifierProvider( + create: (_) => HomeEditorViewModel( + context.read(), + home: home, + ), + child: const HomeEditorView(), + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return CupertinoPageScaffold( + backgroundColor: CupertinoColors.systemGroupedBackground, + navigationBar: CupertinoNavigationBar( + middle: const Text("Home"), + trailing: CupertinoButton( + padding: EdgeInsets.zero, + child: const Icon(CupertinoIcons.add), + onPressed: () => _onEditHome(context), + ), + ), + child: SafeArea( + child: Builder( + builder: (context) { + final homes = context.select( + (HomesViewModel vm) => vm.homes, + ); + + if (homes.isEmpty) { + return Center( + child: CupertinoButton( + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(CupertinoIcons.add), + Text("add your first Home"), + ], + ), + onPressed: () => _onEditHome(context), + ), + ); + } + + return ListView( + children: [ + CupertinoListSection.insetGrouped( + children: List.generate( + homes.length, + (index) { + final home = homes[index]; + return ListEntry( + home: home, + onTap: () => _onEditHome(context, home), + onDismiss: () { + final vm = context.read(); + vm.deleteHome(home); + }, + ); + }, + ), + ), + ], + ); + }, + ), + ), + ); + } +} diff --git a/lib/ui/settings/homes/homes_view_model.dart b/lib/ui/settings/homes/homes_view_model.dart new file mode 100644 index 0000000..3ea394c --- /dev/null +++ b/lib/ui/settings/homes/homes_view_model.dart @@ -0,0 +1,39 @@ +import 'package:dieklingel_app/components/stream_subscription_mixin.dart'; +import 'package:dieklingel_app/repositories/home_repository.dart'; +import 'package:flutter/cupertino.dart'; + +import '../../../models/home.dart'; + +class HomesViewModel extends ChangeNotifier with StreamHandlerMixin { + final HomeRepository homeRepository; + + HomesViewModel(this.homeRepository) { + streams.subscribe( + homeRepository.added, + (Home home) => notifyListeners(), + ); + streams.subscribe( + homeRepository.changed, + (Home home) => notifyListeners(), + ); + streams.subscribe( + homeRepository.removed, + (Home home) => notifyListeners(), + ); + } + + List get homes { + return homeRepository.homes; + } + + void deleteHome(Home home) async { + await homeRepository.delete(home); + notifyListeners(); + } + + @override + void dispose() { + streams.dispose(); + super.dispose(); + } +} diff --git a/lib/ui/settings/homes/widgets/list_entry.dart b/lib/ui/settings/homes/widgets/list_entry.dart new file mode 100644 index 0000000..1e710f5 --- /dev/null +++ b/lib/ui/settings/homes/widgets/list_entry.dart @@ -0,0 +1,43 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import '../../../../models/home.dart'; + +class ListEntry extends StatelessWidget { + final Home home; + final void Function() onTap; + final void Function() onDismiss; + + const ListEntry({ + super.key, + required this.home, + required this.onTap, + required this.onDismiss, + }); + + @override + Widget build(BuildContext context) { + return Dismissible( + key: UniqueKey(), + background: Container( + color: Colors.red, + alignment: Alignment.centerRight, + child: const Padding( + padding: EdgeInsets.only(right: 8.0), + child: Icon( + CupertinoIcons.trash, + color: Colors.white, + ), + ), + ), + direction: DismissDirection.endToStart, + onDismissed: (direction) => onDismiss(), + child: CupertinoListTile( + title: Text(home.name), + onTap: () => onTap(), + leading: const Icon(CupertinoIcons.home), + trailing: const Icon(CupertinoIcons.chevron_forward), + ), + ); + } +} diff --git a/lib/views/settings_view.dart b/lib/ui/settings/settings_view.dart similarity index 83% rename from lib/views/settings_view.dart rename to lib/ui/settings/settings_view.dart index 51bc9b1..737bd34 100644 --- a/lib/views/settings_view.dart +++ b/lib/ui/settings/settings_view.dart @@ -1,15 +1,16 @@ -import 'package:dieklingel_app/blocs/home_list_view_bloc.dart'; -import 'package:dieklingel_app/blocs/ice_server_list_view_bloc.dart'; -import 'package:dieklingel_app/repositories/home_repository.dart'; -import 'package:dieklingel_app/repositories/ice_server_repository.dart'; +import 'package:dieklingel_app/ui/home/home_view_model.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:provider/provider.dart'; -import 'about_view.dart'; +import '../../blocs/ice_server_list_view_bloc.dart'; +import '../../repositories/home_repository.dart'; +import '../../repositories/ice_server_repository.dart'; +import 'about/about_view.dart'; -import '../views/home_list_view.dart'; -import 'ice_server_list_view.dart'; +import '../../views/ice_server_list_view.dart'; +import 'homes/homes_view.dart'; class SettingsView extends StatelessWidget { const SettingsView({super.key}); @@ -31,11 +32,11 @@ class SettingsView extends StatelessWidget { Navigator.push( context, CupertinoPageRoute( - builder: (context) => BlocProvider( - create: (_) => HomeListViewBloc( + builder: (context) => ChangeNotifierProvider( + create: (_) => HomeViewModel( context.read(), ), - child: const HomeListView(), + child: const HomesView(), ), ), ); diff --git a/lib/utils/media_ressource.dart b/lib/utils/media_ressource.dart index 19b4fd0..8b3ccfd 100644 --- a/lib/utils/media_ressource.dart +++ b/lib/utils/media_ressource.dart @@ -13,7 +13,11 @@ class MediaRessource { 'audio': audio, 'video': video, }; - _stream = await navigator.mediaDevices.getUserMedia(constraints); + try { + _stream = await navigator.mediaDevices.getUserMedia(constraints); + } catch (e) { + // TODO: noting stream is empty + } return _stream; } diff --git a/lib/utils/microphone_state.dart b/lib/utils/microphone_state.dart deleted file mode 100644 index 76cdeb5..0000000 --- a/lib/utils/microphone_state.dart +++ /dev/null @@ -1,17 +0,0 @@ -enum MicrophoneState { - muted, - unmuted; - - const MicrophoneState(); - - MicrophoneState next({List skip = const []}) { - int next = (index + 1) % MicrophoneState.values.length; - MicrophoneState state = MicrophoneState.values[next]; - - while (state != this && skip.contains(state)) { - state = state.next(); - } - - return state; - } -} diff --git a/lib/utils/rtc_client_wrapper.dart b/lib/utils/rtc_client_wrapper.dart deleted file mode 100644 index e1dee12..0000000 --- a/lib/utils/rtc_client_wrapper.dart +++ /dev/null @@ -1,227 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_webrtc/flutter_webrtc.dart'; - -import '../models/ice_server.dart'; -import 'media_ressource.dart'; -import 'microphone_state.dart'; -import 'rtc_transceiver.dart'; -import 'speaker_state.dart'; - -class RtcClientWrapper { - MicrophoneState _microphoneState = MicrophoneState.muted; - SpeakerState _speakerState = SpeakerState.muted; - final MediaRessource ressource = MediaRessource(); - final RTCVideoRenderer renderer = RTCVideoRenderer(); - final List servers; - final String uuid; - final List transceivers; - late final RTCPeerConnection connection; - final _state = ValueNotifier( - RTCPeerConnectionState.RTCPeerConnectionStateDisconnected, - ); - bool _isDisposed = false; - final List _candiateBuffer = []; - bool _remoteDescriptionSet = false; - RTCSessionDescription? _offer; - RTCSessionDescription? _answer; - - void Function(RTCIceCandidate)? _onIceCandidateCallback; - - Future offer() async { - if (_isDisposed) { - throw Exception( - "cannot create an offer after the connection has been disposed", - ); - } - if (_offer == null) { - _offer = await connection.createOffer(); - await connection.setLocalDescription(_offer!); - } - return _offer!; - } - - Future setRemoteDescription(RTCSessionDescription answer) async { - if (_remoteDescriptionSet) { - throw "remote already set"; - } - _remoteDescriptionSet = true; - await connection.setRemoteDescription(answer); - for (var candidate in _candiateBuffer) { - connection.addCandidate(candidate); - } - _candiateBuffer.clear(); - } - - Future answer(RTCSessionDescription offer) async { - if (_isDisposed) { - throw Exception( - "cannot create an answer after the connection has been disposed", - ); - } - if (_answer == null) { - await connection.setRemoteDescription(offer); - _answer = await connection.createAnswer(); - } - return _answer!; - } - - MicrophoneState get microphoneState => _microphoneState; - - set microphoneState(MicrophoneState state) { - _microphoneState = state; - _applyMicrophoneSettings(); - } - - SpeakerState get speakerState => _speakerState; - - set speakerState(SpeakerState state) { - _speakerState = state; - _applySpeakerSettings(); - } - - RtcClientWrapper._(this.servers, this.transceivers, this.uuid); - - ValueNotifier get state => _state; - - static Future create({ - List iceServers = const [], - List transceivers = const [], - required String uuid, - }) async { - WidgetsFlutterBinding.ensureInitialized(); - RtcClientWrapper wrapper = RtcClientWrapper._( - iceServers, - transceivers, - uuid, - ); - await wrapper.renderer.initialize(); - - List> servers = iceServers - .map( - (e) => { - "urls": e.urls, - "username": e.username, - "credential": e.credential - }, - ) - .toList(); - - Map configuration = {}; - configuration["iceServers"] = servers; - configuration["sdpSemantics"] = "unified-plan"; - wrapper.connection = await createPeerConnection(configuration); - - wrapper.connection - ..onIceCandidate = (RTCIceCandidate candidate) { - wrapper._onIceCandidateCallback?.call(candidate); - } - ..onConnectionState = wrapper._onConnectionState - ..onTrack = wrapper._onTrack; - - for (RtcTransceiver transceiver in transceivers) { - await wrapper.connection.addTransceiver( - kind: transceiver.kind, - init: RTCRtpTransceiverInit(direction: transceiver.direction), - ); - } - - return wrapper; - } - - void addIceCandidate(RTCIceCandidate candidate) async { - if (_isDisposed) { - return; - } - if (_remoteDescriptionSet) { - await connection.addCandidate(candidate); - } else { - _candiateBuffer.add(candidate); - } - } - - void onIceCandidate(void Function(RTCIceCandidate) handler) { - if (_isDisposed) { - return; - } - _onIceCandidateCallback = handler; - } - - Future dispose() async { - if (_isDisposed) { - return; - } - _isDisposed = true; - ressource.close(); - await connection.close(); - } - - void _onConnectionState(RTCPeerConnectionState state) { - _state.value = state; - switch (state) { - case RTCPeerConnectionState.RTCPeerConnectionStateFailed: - case RTCPeerConnectionState.RTCPeerConnectionStateDisconnected: - break; - default: - break; - } - } - - void _onTrack(RTCTrackEvent event) { - if (event.streams.isEmpty) { - return; - } - - _applyMicrophoneSettings(); - _applySpeakerSettings(media: event.streams); - - renderer.srcObject = event.streams.first; - } - - void _applyMicrophoneSettings() { - switch (_microphoneState) { - case MicrophoneState.muted: - ressource.stream?.getAudioTracks().forEach((track) { - Helper.setMicrophoneMute(true, track); - }); - break; - case MicrophoneState.unmuted: - ressource.stream?.getAudioTracks().forEach((track) { - Helper.setMicrophoneMute(false, track); - }); - break; - } - } - - void _applySpeakerSettings({List media = const []}) async { - final streams = [...connection.getRemoteStreams(), ...media]; - for (final stream in streams) { - if (stream == null) { - continue; - } - switch (_speakerState) { - case SpeakerState.muted: - stream.getAudioTracks().forEach((track) { - track.enabled = false; - }); - break; - case SpeakerState.headphone: - stream.getAudioTracks().forEach((track) { - track.enabled = true; - if (!kIsWeb) { - track.enableSpeakerphone(false); - } - }); - break; - case SpeakerState.speaker: - stream.getAudioTracks().forEach((track) { - track.enabled = true; - if (!kIsWeb) { - track.enableSpeakerphone(false); - } - }); - break; - } - } - } -} diff --git a/lib/utils/rtc_transceiver.dart b/lib/utils/rtc_transceiver.dart deleted file mode 100644 index b330337..0000000 --- a/lib/utils/rtc_transceiver.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:flutter_webrtc/flutter_webrtc.dart'; - -class RtcTransceiver { - RTCRtpMediaType kind; - TransceiverDirection direction; - - RtcTransceiver({required this.kind, required this.direction}); -} diff --git a/lib/utils/speaker_state.dart b/lib/utils/speaker_state.dart deleted file mode 100644 index 696f6cb..0000000 --- a/lib/utils/speaker_state.dart +++ /dev/null @@ -1,18 +0,0 @@ -enum SpeakerState { - muted, - headphone, - speaker; - - const SpeakerState(); - - SpeakerState next({List skip = const []}) { - int next = (index + 1) % SpeakerState.values.length; - SpeakerState state = SpeakerState.values[next]; - - while (state != this && skip.contains(state)) { - state = state.next(); - } - - return state; - } -} diff --git a/lib/views/call_view.dart b/lib/views/call_view.dart deleted file mode 100644 index fc99cf9..0000000 --- a/lib/views/call_view.dart +++ /dev/null @@ -1,211 +0,0 @@ -import 'package:dieklingel_app/blocs/call_view_bloc.dart'; -import 'package:dieklingel_app/components/icon_builder.dart'; -import 'package:dieklingel_app/components/map_builder.dart'; -import 'package:dieklingel_app/states/call_state.dart'; -import 'package:dieklingel_app/utils/microphone_state.dart'; -import 'package:dieklingel_app/utils/speaker_state.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_webrtc/flutter_webrtc.dart'; - -class CallView extends StatelessWidget { - const CallView({super.key}); - - @override - Widget build(BuildContext context) { - return BlocListener( - listener: (context, state) { - if (state is CallCancelState) { - showCupertinoDialog( - context: context, - builder: (_) => CupertinoAlertDialog( - title: const Text("Error"), - content: Text(state.reason), - actions: [ - CupertinoDialogAction( - child: const Text("Ok"), - onPressed: () => Navigator.of(context).pop(), - ), - ], - ), - ); - } - }, - child: Stack( - children: [ - _Video(), - SafeArea( - child: Stack( - children: [ - _Toolbar(), - ], - ), - ), - ], - ), - ); - } -} - -class _Toolbar extends StatelessWidget { - List buttons(BuildContext context, CallState state) { - return [ - _ToolbarButton( - icon: const Icon( - CupertinoIcons.phone_fill, - color: Colors.white, - size: 30, - ), - color: MapBuilder( - values: { - CallActiveState: Colors.red, - CallInitatedState: Colors.red, - }, - fallback: Colors.green, - id: state.runtimeType, - ).build(), - onPressed: () { - context.read().add( - state is CallActiveState || state is CallInitatedState - ? CallHangup() - : CallStart(), - ); - }, - ), - _ToolbarButton( - icon: IconBuilder( - values: { - MicrophoneState.muted: const Icon(CupertinoIcons.mic_slash_fill), - MicrophoneState.unmuted: const Icon(CupertinoIcons.mic_fill) - }, - fallback: const Icon(CupertinoIcons.mic_slash_fill), - id: state is CallActiveState ? state.microphoneState : null, - ).build( - color: Colors.white, - size: 30, - ), - color: MapBuilder( - values: { - MicrophoneState.muted: Colors.green, - MicrophoneState.unmuted: Colors.red, - }, - fallback: Colors.green, - id: state is CallActiveState ? state.microphoneState : null, - ).build(), - onPressed: state is CallActiveState - ? () { - context.read().add(CallToogleMicrophone()); - } - : null, - ), - _ToolbarButton( - icon: IconBuilder( - values: { - SpeakerState.muted: const Icon(CupertinoIcons.speaker_slash_fill), - SpeakerState.headphone: const Icon(CupertinoIcons.speaker_1_fill), - SpeakerState.speaker: const Icon(CupertinoIcons.speaker_3_fill), - }, - fallback: const Icon(CupertinoIcons.speaker_slash_fill), - id: state is CallActiveState ? state.speakerState : null, - ).build( - color: Colors.white, - size: 30, - ), - color: MapBuilder( - values: { - SpeakerState.muted: Colors.green, - SpeakerState.headphone: Colors.orange, - SpeakerState.speaker: Colors.red, - }, - fallback: Colors.green, - id: state is CallActiveState ? state.speakerState : null, - ).build(), - onPressed: state is CallActiveState - ? () { - context.read().add(CallToogleSpeaker()); - } - : null, - ), - const _ToolbarButton( - icon: Icon( - CupertinoIcons.lock_fill, - color: Colors.white, - size: 30, - ), - color: Colors.amber, - ), - ]; - } - - @override - Widget build(BuildContext context) { - return Align( - alignment: Alignment.bottomCenter, - child: BlocBuilder( - builder: (context, state) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - mainAxisSize: MainAxisSize.max, - children: buttons(context, state), - ); - }, - ), - ); - } -} - -class _ToolbarButton extends StatelessWidget { - final Icon icon; - final Color color; - final void Function()? onPressed; - - const _ToolbarButton({ - required this.icon, - required this.color, - this.onPressed, - }); - - @override - Widget build(BuildContext context) { - return CupertinoButton( - onPressed: onPressed, - child: Container( - padding: const EdgeInsets.all(15.0), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: onPressed == null ? Colors.black26 : color, - ), - child: icon, - ), - ); - } -} - -class _Video extends StatelessWidget { - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - if (state is CallInitatedState) { - return const Center( - child: CupertinoActivityIndicator(), - ); - } - - if (state is CallActiveState) { - return ValueListenableBuilder( - valueListenable: state.renderer, - builder: (c, v, w) => InteractiveViewer( - child: RTCVideoView( - state.renderer, - ), - ), - ); - } - - return Container(); - }, - ); - } -} diff --git a/lib/views/home_add_view.dart b/lib/views/home_add_view.dart deleted file mode 100644 index 3a75a26..0000000 --- a/lib/views/home_add_view.dart +++ /dev/null @@ -1,158 +0,0 @@ -import 'package:dieklingel_app/blocs/home_add_view_bloc.dart'; -import 'package:dieklingel_app/states/home_add_state.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:path/path.dart' as path; -import '../models/hive_home.dart'; - -class HomeAddView extends StatefulWidget { - final HiveHome? home; - - const HomeAddView({super.key, this.home}); - - @override - State createState() => _HomeAddView(); -} - -class _HomeAddView extends State { - late final _name = TextEditingController(text: widget.home?.name); - late final _server = TextEditingController( - text: widget.home == null - ? null - : "${widget.home!.uri.scheme}://${widget.home!.uri.host}:${widget.home!.uri.port}", - ); - late final _username = TextEditingController(text: widget.home?.username); - late final _password = TextEditingController(text: widget.home?.password); - late final _channel = TextEditingController( - text: widget.home == null - ? null - : path.normalize("./${widget.home!.uri.path}"), - ); - late final _sign = TextEditingController(text: widget.home?.uri.fragment); - - @override - Widget build(BuildContext context) { - return BlocConsumer( - listener: (context, state) { - if (state is HomeAddSuccessfulState) { - Navigator.of(context).pop(); - } - if (state is HomeAddErrorState) { - showCupertinoDialog( - context: context, - builder: (BuildContext context) { - return CupertinoAlertDialog( - title: const Text("Error"), - content: Text(state.errorMessage), - actions: [ - CupertinoButton( - child: const Text("Ok"), - onPressed: () { - Navigator.of(context).pop(); - }, - ) - ], - ); - }, - ); - } - }, - builder: (context, state) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - leading: CupertinoButton( - padding: EdgeInsets.zero, - child: const Text("Cancel"), - onPressed: () { - Navigator.of(context).pop(); - }), - middle: const Text("Home"), - trailing: state is HomeAddLoadingState - ? const CupertinoActivityIndicator() - : CupertinoButton( - padding: EdgeInsets.zero, - onPressed: () { - context.read().add( - HomeAddSubmit( - home: widget.home, - name: _name.text, - server: _server.text, - username: _username.text, - password: _password.text, - channel: _channel.text, - sign: _sign.text, - ), - ); - }, - child: const Text("Save"), - ), - ), - backgroundColor: CupertinoColors.systemGroupedBackground, - child: SafeArea( - child: ListView( - clipBehavior: Clip.none, - children: [ - CupertinoFormSection.insetGrouped( - header: const Text("Configuration"), - children: [ - CupertinoTextFormFieldRow( - prefix: const Text("Name"), - controller: _name, - validator: (value) => state is HomeAddFormErrorState - ? state.nameError - : null, - autovalidateMode: AutovalidateMode.always, - ), - ], - ), - CupertinoFormSection.insetGrouped( - header: const Text("Server"), - children: [ - CupertinoTextFormFieldRow( - prefix: const Text("Server URL"), - controller: _server, - validator: (value) => state is HomeAddFormErrorState - ? state.serverError - : null, - autovalidateMode: AutovalidateMode.always, - ), - CupertinoTextFormFieldRow( - prefix: const Text("Username"), - controller: _username, - ), - CupertinoTextFormFieldRow( - prefix: const Text("Password"), - obscureText: true, - controller: _password, - ), - ], - ), - CupertinoFormSection.insetGrouped( - header: const Text("Channel"), - children: [ - CupertinoTextFormFieldRow( - prefix: const Text("Channel Prefix"), - validator: (value) => state is HomeAddFormErrorState - ? state.channelError - : null, - autovalidateMode: AutovalidateMode.always, - controller: _channel, - ), - CupertinoTextFormFieldRow( - prefix: const Text("Sign"), - validator: (value) => state is HomeAddFormErrorState - ? state.signError - : null, - autovalidateMode: AutovalidateMode.always, - controller: _sign, - ) - ], - ) - ], - ), - ), - ); - }, - ); - } -} diff --git a/lib/views/home_list_view.dart b/lib/views/home_list_view.dart deleted file mode 100644 index 44b79b8..0000000 --- a/lib/views/home_list_view.dart +++ /dev/null @@ -1,149 +0,0 @@ -import 'package:dieklingel_app/blocs/home_list_view_bloc.dart'; -import 'package:dieklingel_app/states/home_list_state.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../models/hive_home.dart'; -import 'home_add_view.dart'; - -class HomeListView extends StatelessWidget { - const HomeListView({super.key}); - - void _onEditHome(BuildContext context, [HiveHome? home]) async { - final bloc = context.read(); - - await Navigator.push( - context, - CupertinoModalPopupRoute( - builder: (context) => CupertinoPopupSurface( - child: HomeAddView( - home: home, - ), - ), - ), - ); - bloc.add(HomeListRefresh()); - } - - @override - Widget build(BuildContext context) { - return CupertinoPageScaffold( - backgroundColor: CupertinoColors.systemGroupedBackground, - navigationBar: CupertinoNavigationBar( - middle: const Text("Home"), - trailing: CupertinoButton( - padding: EdgeInsets.zero, - child: const Icon(CupertinoIcons.add), - onPressed: () => _onEditHome(context), - ), - ), - child: SafeArea(child: BlocBuilder( - builder: (context, state) { - if (state.homes.isEmpty) { - return Center( - child: CupertinoButton( - child: const Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(CupertinoIcons.add), - Text("add your first Home"), - ], - ), - onPressed: () => _onEditHome(context), - ), - ); - } - - return ListView( - children: [ - CupertinoListSection.insetGrouped( - children: [ - for (HiveHome home in state.homes) ...[ - Dismissible( - key: UniqueKey(), - background: Container( - color: Colors.red, - alignment: Alignment.centerRight, - child: const Padding( - padding: EdgeInsets.only(right: 8.0), - child: Icon( - CupertinoIcons.trash, - color: Colors.white, - ), - ), - ), - direction: DismissDirection.endToStart, - onDismissed: (direction) async { - context - .read() - .add(HomeListDeleted(home: home)); - }, - child: CupertinoListTile( - title: Text(home.name), - onTap: () => _onEditHome(context, home), - leading: const Icon(CupertinoIcons.home), - trailing: const Icon(CupertinoIcons.chevron_forward), - ), - ), - ], - ], - ), - ], - ); - }, - ) - - /* StreamBuilder( - stream: context.bloc().homes, - builder: ( - BuildContext context, - AsyncSnapshot> snapshot, - ) { - if (!snapshot.hasData) { - return const Padding( - padding: EdgeInsets.all(20), - child: CupertinoActivityIndicator(), - ); - } - - List homes = snapshot.data!; - - return ListView.builder( - itemCount: homes.length, - itemBuilder: (context, index) { - HiveHome home = homes[index]; - - return Dismissible( - key: UniqueKey(), - background: Container( - color: Colors.red, - alignment: Alignment.centerRight, - child: const Padding( - padding: EdgeInsets.only(right: 8.0), - child: Icon( - CupertinoIcons.trash, - color: Colors.white, - ), - ), - ), - direction: DismissDirection.endToStart, - onDismissed: (direction) async { - await home.delete(); - }, - child: CupertinoInkWell( - onTap: () => _onHomeEditViewPressed(context, home), - child: CupertinoFormRow( - prefix: Text(home.name), - child: const Icon(CupertinoIcons.forward), - ), - ), - ); - }, - ); - }, - )),*/ - ), - ); - } -} diff --git a/lib/views/home_view.dart b/lib/views/home_view.dart deleted file mode 100644 index 43dc9ce..0000000 --- a/lib/views/home_view.dart +++ /dev/null @@ -1,163 +0,0 @@ -import 'package:dieklingel_app/blocs/home_view_bloc.dart'; -import 'package:dieklingel_app/states/call_state.dart'; -import 'package:dieklingel_app/states/home_state.dart'; -import 'package:dieklingel_app/views/call_view.dart'; -import 'package:dieklingel_app/views/home_add_view.dart'; -import 'package:dieklingel_app/views/ice_server_add_view.dart'; -import 'package:dieklingel_app/views/settings_view.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:pull_down_button/pull_down_button.dart'; - -import '../blocs/call_view_bloc.dart'; -import '../models/hive_home.dart'; - -class HomeView extends StatelessWidget { - const HomeView({super.key}); - - void _onAddHome(BuildContext context) async { - final bloc = context.read(); - await showCupertinoModalPopup( - context: context, - builder: (context) { - return const CupertinoPopupSurface( - child: HomeAddView(), - ); - }, - ); - bloc.add(HomeRefresh()); - } - - void _onAddIceServer(BuildContext context) async { - final bloc = context.read(); - await showCupertinoModalPopup( - context: context, - builder: (context) { - return const CupertinoPopupSurface( - child: IceServerAddView(), - ); - }, - ); - bloc.add(HomeRefresh()); - } - - void _onSettingsTap(BuildContext context) async { - final bloc = context.read(); - await Navigator.of(context).push( - CupertinoPageRoute( - builder: (context) => const SettingsView(), - ), - ); - bloc.add(HomeRefresh()); - } - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: Text(state is HomeSelectedState ? state.home.name : "Home"), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - PullDownButton( - itemBuilder: (context) => [ - PullDownMenuItem( - onTap: () => _onAddHome(context), - title: "add Home", - icon: CupertinoIcons.home, - ), - const PullDownMenuDivider(), - PullDownMenuItem( - onTap: () => _onAddIceServer(context), - title: "add ICE Server", - icon: CupertinoIcons.cloud, - ) - ], - buttonBuilder: (context, showMenu) => CupertinoButton( - padding: EdgeInsets.zero, - onPressed: showMenu, - child: const Icon(CupertinoIcons.plus), - ), - ), - PullDownButton( - itemBuilder: (context) => [ - PullDownMenuItem( - onTap: () => _onSettingsTap(context), - title: "Settings", - icon: CupertinoIcons.settings, - ), - if (state.homes.isNotEmpty) ...[ - const PullDownMenuDivider.large(), - ], - for (HiveHome home in state.homes) ...[ - PullDownMenuItem.selectable( - selected: - state is HomeSelectedState && home == state.home, - onTap: () { - context - .read() - .add(HomeSelected(home: home)); - context.read().add(CallHangup()); - }, - title: home.name, - ), - if (home != state.homes.last) ...[ - const PullDownMenuDivider() - ], - ] - ], - buttonBuilder: (context, showMenu) => CupertinoButton( - padding: EdgeInsets.zero, - onPressed: showMenu, - child: const Icon(CupertinoIcons.ellipsis_circle), - ), - ), - ], - ), - ), - child: _Content(), - ); - }, - ); - } -} - -class _Content extends StatelessWidget { - void _onAddHome(BuildContext context) async { - final bloc = context.read(); - await showCupertinoModalPopup( - context: context, - builder: (context) { - return const CupertinoPopupSurface( - child: HomeAddView(), - ); - }, - ); - bloc.add(HomeRefresh()); - } - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: ((context, state) { - if (state is! HomeSelectedState) { - return Center( - child: CupertinoButton( - child: const Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(CupertinoIcons.add), - Text("add your first Home"), - ], - ), - onPressed: () => _onAddHome(context), - ), - ); - } - return const CallView(); - }), - ); - } -} diff --git a/packages/mqtt/lib/mqtt.dart b/packages/mqtt/lib/mqtt.dart index fefacbe..b3c5b8e 100644 --- a/packages/mqtt/lib/mqtt.dart +++ b/packages/mqtt/lib/mqtt.dart @@ -1,4 +1,4 @@ library mqtt; -export 'src/mqtt_client.dart'; -export 'src/subscription.dart'; +export 'src/client.dart'; +export 'src/connnection_state.dart'; diff --git a/packages/mqtt/lib/src/mqtt_client.dart b/packages/mqtt/lib/src/client.dart similarity index 53% rename from packages/mqtt/lib/src/mqtt_client.dart rename to packages/mqtt/lib/src/client.dart index 317da26..f7187e2 100644 --- a/packages/mqtt/lib/src/mqtt_client.dart +++ b/packages/mqtt/lib/src/client.dart @@ -1,39 +1,77 @@ import 'dart:async'; import 'dart:convert'; +import 'package:mqtt/mqtt.dart'; + import 'factories/mqtt_client_factory.dart'; -import 'subscription.dart'; import 'package:uuid/uuid.dart'; import 'package:mqtt_client/mqtt_client.dart' as mqtt; -class MqttClient { +class Client { final Uri _uri; final mqtt.MqttClient _client; - final Map> _subscriptions = {}; + final Map>> _subscriptions = + {}; + Function(ConnectionState)? onConnectionStateChanged; - MqttClient._(this._client, this._uri) { + Client._(this._client, this._uri) { _client ..port = _uri.port ..keepAlivePeriod = 20 ..setProtocolV311() ..autoReconnect = true; + _client + ..onDisconnected = () { + onConnectionStateChanged?.call(state); + } + ..onConnected = () { + onConnectionStateChanged?.call(state); + } + ..onAutoReconnected = () { + onConnectionStateChanged?.call(state); + } + ..onAutoReconnect = () { + onConnectionStateChanged?.call(state); + }; } - factory MqttClient(Uri uri, {String? identifier}) { + ConnectionState get state { + return _client.connectionStatus?.state ?? ConnectionState.disconnected; + } + + factory Client(Uri uri, {String? identifier}) { final client = const MqttClientFactory().create( uri, identifier ?? const Uuid().v4(), ); - return MqttClient._(client, uri); + return Client._(client, uri); } void disconnect() { _client.disconnect(); } - Future connect({String username = "", String password = ""}) async { - await _client.connect(username, password); + Future connect({ + String username = "", + String password = "", + bool throws = true, + }) async { + if (state == ConnectionState.connected) { + _client.disconnect(); + onConnectionStateChanged?.call(state); + await Future.delayed(const Duration(milliseconds: 50)); + } + + try { + await _client.connect(username, password); + } catch (e) { + if (throws) { + rethrow; + } + onConnectionStateChanged?.call(state); + return; + } _client.updates!.listen((event) { mqtt.MqttPublishMessage rec = event[0].payload as mqtt.MqttPublishMessage; final String topic = event[0].topic; @@ -45,17 +83,16 @@ class MqttClient { final pt = mqtt.PublicationTopic(topic); if (st.matches(pt)) { - for (Subscription sub in entry.value) { - sub.callback(topic, message); + for (StreamController<(String, String)> sub in entry.value) { + sub.add((topic, message)); } } } }); } - Subscription subscribe( - String topic, - Callback callback, { + Stream<(String, String)> topic( + String topic, { mqtt.MqttQos qosLevel = mqtt.MqttQos.exactlyOnce, }) { final mqtt.Subscription? sub = _client.subscribe(topic, qosLevel); @@ -64,23 +101,26 @@ class MqttClient { throw Exception("error while subscribing to $topic"); } - final subscription = Subscription( - this, - callback, - sub, - ); + final controller = StreamController<(String, String)>(); + controller.onCancel = () { + List? subs = _subscriptions[topic]; + subs?.remove(controller); + if (subs != null && subs.isEmpty) { + _client.unsubscribe(topic); + } + }; + _subscriptions.putIfAbsent(topic, () => []).add(controller); - _subscriptions.putIfAbsent(topic, () => []).add(subscription); - return subscription; + return controller.stream; } - void unsubscribe(Subscription subscription) { + /* void unsubscribe(Subscription subscription) { List? subs = _subscriptions[subscription.subscription.topic]; subs?.remove(subscription); if (subs != null && subs.isEmpty) { _client.unsubscribe(subscription.subscription.topic.toString()); } - } + }*/ Future publish( String topic, @@ -96,7 +136,7 @@ class MqttClient { await Future.delayed(const Duration(seconds: 5)); } - Future once(String topic, {Duration? timeout}) async { + /* Future once(String topic, {Duration? timeout}) async { Completer completer = Completer(); Subscription subscription = subscribe(topic, (topic, message) { completer.complete(message); @@ -109,5 +149,5 @@ class MqttClient { } subscription.cancel(); return result; - } + }*/ } diff --git a/packages/mqtt/lib/src/connnection_state.dart b/packages/mqtt/lib/src/connnection_state.dart index 2fa8221..7cd7a48 100644 --- a/packages/mqtt/lib/src/connnection_state.dart +++ b/packages/mqtt/lib/src/connnection_state.dart @@ -1,3 +1,3 @@ -enum ConnectionState { - connected, -} +import 'package:mqtt_client/mqtt_client.dart' as mqtt; + +typedef ConnectionState = mqtt.MqttConnectionState; diff --git a/packages/mqtt/lib/src/subscription.dart b/packages/mqtt/lib/src/subscription.dart deleted file mode 100644 index 988ed38..0000000 --- a/packages/mqtt/lib/src/subscription.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'mqtt_client.dart'; -import 'package:mqtt_client/mqtt_client.dart' as mqtt; - -typedef Callback = void Function(String topic, String message); - -class Subscription { - final MqttClient client; - final Callback callback; - final mqtt.Subscription subscription; - - Subscription(this.client, this.callback, this.subscription); - - void cancel() { - client.unsubscribe(this); - } -} diff --git a/pubspec.lock b/pubspec.lock index aab356e..15a25a4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,26 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 url: "https://pub.dev" source: hosted - version: "61.0.0" + version: "64.0.0" _flutterfire_internals: dependency: transitive description: name: _flutterfire_internals - sha256: "1a5e13736d59235ce0139621b4bbe29bc89839e202409081bc667eb3cd20674c" + sha256: dd68ecea9f1e3556d385521bd21c7bafd6311a8c1e11abe2595ca27974f468ee url: "https://pub.dev" source: hosted - version: "1.3.5" + version: "1.3.13" analyzer: dependency: transitive description: name: analyzer - sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" url: "https://pub.dev" source: hosted - version: "5.13.0" + version: "6.2.0" args: dependency: transitive description: @@ -45,10 +45,10 @@ packages: dependency: "direct main" description: name: audio_session - sha256: "8a2bc5e30520e18f3fb0e366793d78057fb64cd5287862c76af0c8771f2a52ad" + sha256: "6fdf255ed3af86535c96452c33ecff1245990bb25a605bfb1958661ccc3d467f" url: "https://pub.dev" source: hosted - version: "0.1.16" + version: "0.1.18" bloc: dependency: transitive description: @@ -57,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.2" + blueprint: + dependency: "direct main" + description: + name: blueprint + sha256: d30dc7123090d7ee97fa30daf2aeb496964340358d0d4aa6077d8852bb984058 + url: "https://pub.dev" + source: hosted + version: "0.0.3" boolean_selector: dependency: transitive description: @@ -85,10 +93,10 @@ packages: dependency: transitive description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.18.0" convert: dependency: transitive description: @@ -101,10 +109,10 @@ packages: dependency: transitive description: name: coverage - sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097" + sha256: ac86d3abab0f165e4b8f561280ff4e066bceaac83c424dd19f1ae2c2fcd12ca9 url: "https://pub.dev" source: hosted - version: "1.6.3" + version: "1.7.1" crypto: dependency: transitive description: @@ -117,26 +125,26 @@ packages: dependency: "direct main" description: name: cupertino_icons - sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.6" dart_webrtc: dependency: transitive description: name: dart_webrtc - sha256: dfe42714abe3eb83eefec407c9da7f8e341a899aa1b8ac2484af298cdfeb74a3 + sha256: "5897a3bdd6c7fded07e80e250260ca4c9cd61f9080911aa308b516e1206745a9" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" dbus: dependency: transitive description: name: dbus - sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" url: "https://pub.dev" source: hosted - version: "0.7.8" + version: "0.7.10" event_bus: dependency: transitive description: @@ -165,58 +173,58 @@ packages: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: c78132175edda4bc532a71e01a32964e4b4fcf53de7853a422d96dac3725f389 + sha256: "471b46ea6a9af503184d4de691566887daedd312aec5baac5baa42d819f56446" url: "https://pub.dev" source: hosted - version: "2.15.1" + version: "2.23.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: b63e3be6c96ef5c33bdec1aab23c91eb00696f6452f0519401d640938c94cba2 + sha256: c437ae5d17e6b5cc7981cf6fd458a5db4d12979905f9aafd1fea930428a9fe63 url: "https://pub.dev" source: hosted - version: "4.8.0" + version: "5.0.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: "4cf4d2161530332ddc3c562f19823fb897ff37a9a774090d28df99f47370e973" + sha256: "0631a2ec971dbc540275e2fa00c3a8a2676f0a7adbc3c197d6fba569db689d97" url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "2.8.1" firebase_messaging: dependency: "direct main" description: name: firebase_messaging - sha256: db4a38be54fd84849c21be1ae1b44f0d4637eec1069bf5c49ea95e81f582bbc0 + sha256: f4576000e749c906d44649feadb2449ada39eab9777650319bdd6fa69a3044f5 url: "https://pub.dev" source: hosted - version: "14.6.6" + version: "14.7.5" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: "164119eed47ff19284e28bea9165a03da110c56ea09dd996622cfccad14d0efd" + sha256: dcf065ecb9518ddc671477f9b8592274df7ccf74c6aaa15f02fc92b1f2b9c8a8 url: "https://pub.dev" source: hosted - version: "4.5.5" + version: "4.5.14" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: "6196d20731733834d7afb175c4345be57ddbd5daebca83cd52a430d62c2279fe" + sha256: "11b7920273367ce2cb272d29153bb7aaae93577eba1eba423c7af61d7f2a3eb2" url: "https://pub.dev" source: hosted - version: "3.5.5" + version: "3.5.14" flutter: dependency: "direct main" description: flutter @@ -234,34 +242,34 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4" + sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "3.0.1" flutter_local_notifications: dependency: "direct main" description: name: flutter_local_notifications - sha256: "293995f94e120c8afce768981bd1fa9c5d6de67c547568e3b42ae2defdcbb4a0" + sha256: "6d11ea777496061e583623aaf31923f93a9409ef8fcaeeefdd6cd78bf4fe5bb3" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "16.1.0" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - sha256: ccb08b93703aeedb58856e5637450bf3ffec899adb66dc325630b68994734b89 + sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" url: "https://pub.dev" source: hosted - version: "3.0.0+1" + version: "4.0.0+1" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "5ec1feac5f7f7d9266759488bc5f76416152baba9aa1b26fe572246caa00d1ab" + sha256: "7cf643d6d5022f3baed0be777b0662cce5919c0a7b86e700299f22dc4ae660ef" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "7.0.0+1" flutter_test: dependency: "direct dev" description: flutter @@ -276,10 +284,10 @@ packages: dependency: "direct main" description: name: flutter_webrtc - sha256: "770c6f8babfdc4907539dc57bf9e98b89132eaa4486bac774c537dd25c2d5362" + sha256: "8522e9f347aed9f03ec591d05fc286a698c1b11a1a6d3e994e92727d24c6f352" url: "https://pub.dev" source: hosted - version: "0.9.40" + version: "0.9.46" frontend_server_client: dependency: transitive description: @@ -316,18 +324,10 @@ packages: dependency: "direct main" description: name: http - sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" - url: "https://pub.dev" - source: hosted - version: "0.13.6" - http_methods: - dependency: transitive - description: - name: http_methods - sha256: "6bccce8f1ec7b5d701e7921dca35e202d425b57e317ba1a37f2638590e29e566" + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.0" http_multi_server: dependency: transitive description: @@ -364,10 +364,10 @@ packages: dependency: transitive description: name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "3.0.0" logging: dependency: transitive description: @@ -380,26 +380,26 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.5.0" meta: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" mime: dependency: transitive description: @@ -459,82 +459,130 @@ packages: dependency: transitive description: name: path_provider - sha256: "909b84830485dbcd0308edf6f7368bc8fd76afa26a270420f34cabea2a6467a0" + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "5d44fc3314d969b84816b569070d7ace0f1dea04bd94a83f74c4829615d22ad8" + sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.1" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "1b744d3d774e5a879bb76d6cd1ecee2ba2c6960c03b1020cd35212f6aa267ac5" + sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: ba2b77f0c52a33db09fc8caf85b12df691bf28d983e84cf87ff6d693cfa007b3 + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: bced5679c7df11190e1ddc35f3222c858f328fff85c3942e46e7f5589bf9eb84 + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: ee0e0d164516b90ae1f970bdf29f726f1aa730d7cfc449ecc74c495378b705da + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "284a66179cabdf942f838543e10413246f06424d960c92ba95c84439154fcac8" + url: "https://pub.dev" + source: hosted + version: "11.0.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: f9fddd3b46109bd69ff3f9efa5006d2d309b7aec0f3c1c5637a60a2d5659e76e + url: "https://pub.dev" + source: hosted + version: "11.1.0" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5" + url: "https://pub.dev" + source: hosted + version: "9.1.4" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4" + url: "https://pub.dev" + source: hosted + version: "3.12.0" + permission_handler_web: + dependency: "direct main" + description: + name: permission_handler_web + sha256: "78255957b505ae852b51d8894655f826ee48ad4b805c9552d8035a93b3ea9247" + url: "https://pub.dev" + source: hosted + version: "0.0.2" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098 + url: "https://pub.dev" + source: hosted + version: "0.1.3" petitparser: dependency: transitive description: name: petitparser - sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + sha256: eeb2d1428ee7f4170e2bd498827296a18d4e7fc462b71727d111c0ac7707cfa6 url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "6.0.1" platform: dependency: transitive description: name: platform - sha256: "57c07bf82207aee366dfaa3867b3164e4f03a238a461a11b0e8a3a510d51203d" + sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.3" platform_detect: dependency: transitive description: name: platform_detect - sha256: "14afcb6ffcd93745e39a288db53d1d6522ea25d71f7993c13a367a86c437b54d" + sha256: "08f4ee79c0e1c4858d37e06b22352a3ebdef5466b613749a3adb03e703d4f5b0" url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.0.11" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd" + sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8 url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.1.7" pool: dependency: transitive description: @@ -543,22 +591,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" - process: - dependency: transitive - description: - name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" - url: "https://pub.dev" - source: hosted - version: "4.2.4" provider: - dependency: transitive + dependency: "direct main" description: name: provider - sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" url: "https://pub.dev" source: hosted - version: "6.0.5" + version: "6.1.1" pub_semver: dependency: transitive description: @@ -571,10 +611,10 @@ packages: dependency: "direct main" description: name: pull_down_button - sha256: "7b19903d04c1768e15e19b31e90921c16ad26daac9512b8a215699d0774b6104" + sha256: "235b302701ce029fd9e9470975069376a6700935bb47a5f1b3ec8a5efba07e6f" url: "https://pub.dev" source: hosted - version: "0.8.3" + version: "0.9.3" rxdart: dependency: transitive description: @@ -584,7 +624,7 @@ packages: source: hosted version: "0.27.7" shelf: - dependency: "direct main" + dependency: transitive description: name: shelf sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 @@ -599,14 +639,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" - shelf_router: - dependency: "direct main" - description: - name: shelf_router - sha256: f5e5d492440a7fb165fe1e2e1a623f31f734d3370900070b2b1e0d0428d59864 - url: "https://pub.dev" - source: hosted - version: "1.1.4" shelf_static: dependency: transitive description: @@ -648,26 +680,26 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" string_scanner: dependency: transitive description: @@ -688,26 +720,26 @@ packages: dependency: "direct dev" description: name: test - sha256: "3dac9aecf2c3991d09b9cdde4f98ded7b30804a88a0d7e4e7e1678e78d6b97f4" + sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f url: "https://pub.dev" source: hosted - version: "1.24.1" + version: "1.24.9" test_api: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.6.1" test_core: dependency: transitive description: name: test_core - sha256: "5138dbffb77b2289ecb12b81c11ba46036590b72a64a7a90d6ffb880f1a29e93" + sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.5.9" timezone: dependency: transitive description: @@ -728,66 +760,66 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e" + sha256: b1c9e98774adf8820c96fbc7ae3601231d324a7d5ebd8babe27b6dfac91357ba url: "https://pub.dev" source: hosted - version: "6.1.12" + version: "6.2.1" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "3dd2388cc0c42912eee04434531a26a82512b9cb1827e0214430c9bcbddfe025" + sha256: "31222ffb0063171b526d3e569079cf1f8b294075ba323443fdc690842bfd4def" url: "https://pub.dev" source: hosted - version: "6.0.38" + version: "6.2.0" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" + sha256: bba3373219b7abb6b5e0d071b0fe66dfbe005d07517a68e38d4fc3638f35c6d3 url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "6.2.1" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" + sha256: "9f2d390e096fdbe1e6e6256f97851e51afc2d9c423d3432f1d6a02a8a9a8b9fd" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.1.0" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "1c4fdc0bfea61a70792ce97157e5cc17260f61abbe4f39354513f39ec6fd73b1" + sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.1.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea + sha256: "980e8d9af422f477be6948bdfb68df8433be71f5743a188968b0c1b887807e50" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.2.0" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4 + sha256: "138bd45b3a456dcfafc46d1a146787424f8d2edfbf2809c9324361e58f851cf7" url: "https://pub.dev" source: hosted - version: "2.0.18" + version: "2.2.1" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422" + sha256: "7754a1ad30ee896b265f8d14078b0513a4dba28d358eabb9d5f339886f4a1adc" url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "3.1.0" uuid: dependency: "direct main" description: @@ -808,10 +840,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0fae432c85c4ea880b33b497d32824b97795b04cdaa74d270219572a1f50268d" + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 url: "https://pub.dev" source: hosted - version: "11.9.0" + version: "13.0.0" watcher: dependency: transitive description: @@ -820,6 +852,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + url: "https://pub.dev" + source: hosted + version: "0.3.0" web_socket_channel: dependency: transitive description: @@ -832,42 +872,42 @@ packages: dependency: transitive description: name: webkit_inspection_protocol - sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" webrtc_interface: dependency: transitive description: name: webrtc_interface - sha256: faec2b578f7cd588766843a8c59d4a0137c44de10b83341ce7bec05e104614d7 + sha256: "2efbd3e4e5ebeb2914253bcc51dafd3053c4b87b43f3076c74835a9deecbae3a" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" win32: dependency: transitive description: name: win32 - sha256: f2add6fa510d3ae152903412227bda57d0d5a8da61d2c39c1fb022c9429a41c0 + sha256: "7c99c0e1e2fa190b48d25c81ca5e42036d5cac81430ef249027d97b0935c553f" url: "https://pub.dev" source: hosted - version: "5.0.6" + version: "5.1.0" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 + sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" url: "https://pub.dev" source: hosted - version: "0.2.0+3" + version: "1.0.3" xml: dependency: transitive description: name: xml - sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + sha256: af5e77e9b83f2f4adc5d3f0a4ece1c7f45a2467b695c2540381bac793e34e556 url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.4.2" yaml: dependency: transitive description: @@ -877,5 +917,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.0.6 <3.7.11" - flutter: ">=3.10.0" + dart: ">=3.2.0 <3.7.11" + flutter: ">=3.16.0" diff --git a/pubspec.yaml b/pubspec.yaml index c2cf443..44c8859 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,10 +15,10 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.2.2+1 +version: 1.2.3+1 environment: - sdk: ">=2.18.6 <3.7.11" + sdk: ">=3.0.0 <3.7.11" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -36,24 +36,26 @@ dependencies: # use version 0.9.22 caused by ios speakerphone issue: # https://github.com/flutter-webrtc/flutter-webrtc/issues/1328 # https://github.com/flutter-webrtc/flutter-webrtc/issues/1326 - flutter_webrtc: ^0.9.36 + flutter_webrtc: ^0.9.46 firebase_core: ^2.6.1 firebase_messaging: ^14.4.0 audio_session: ^0.1.13 uuid: ^3.0.6 - flutter_local_notifications: ^13.0.0 + flutter_local_notifications: ^16.1.0 url_launcher: ^6.1.6 hive_flutter: ^1.1.0 mqtt: path: packages/mqtt flutter_bloc: ^8.1.2 - pull_down_button: ^0.8.1 - shelf_router: ^1.1.4 - shelf: ^1.4.1 - http: ^0.13.6 + pull_down_button: ^0.9.3 + http: ^1.1.0 async: ^2.11.0 path: ^1.8.3 mqtt_client: ^10.0.0 + provider: ^6.0.5 + blueprint: ^0.0.3 + permission_handler: ^11.0.1 + permission_handler_web: ^0.0.2 dev_dependencies: flutter_test: @@ -65,7 +67,7 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^2.0.1 + flutter_lints: ^3.0.1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec @@ -78,8 +80,8 @@ flutter: uses-material-design: true # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg + assets: + - assets/images/house.png # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see diff --git a/test/utils/microphone_state_test.dart b/test/utils/microphone_state_test.dart deleted file mode 100644 index c265aaf..0000000 --- a/test/utils/microphone_state_test.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:dieklingel_app/utils/microphone_state.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group("MicrophoneState", () { - group(".next()", () { - test("without skip; muted to unmuted", () { - MicrophoneState state = MicrophoneState.muted; - - MicrophoneState actual = state.next(); - MicrophoneState expected = MicrophoneState.unmuted; - - expect(actual, equals(expected)); - }); - - test("without skip; unmuted to muted", () { - MicrophoneState state = MicrophoneState.unmuted; - - MicrophoneState actual = state.next(); - MicrophoneState expected = MicrophoneState.muted; - - expect(actual, equals(expected)); - }); - - test("skip all; muted to muted", () { - MicrophoneState state = MicrophoneState.muted; - - MicrophoneState actual = state.next(skip: MicrophoneState.values); - MicrophoneState expected = MicrophoneState.muted; - - expect(actual, equals(expected)); - }); - - test("skip unmuted; muted to muted", () { - MicrophoneState state = MicrophoneState.muted; - - MicrophoneState actual = state.next(skip: [MicrophoneState.unmuted]); - MicrophoneState expected = MicrophoneState.muted; - - expect(actual, equals(expected)); - }); - - test("skip muted; muted to unmuted", () { - MicrophoneState state = MicrophoneState.muted; - - MicrophoneState actual = state.next(skip: [MicrophoneState.muted]); - MicrophoneState expected = MicrophoneState.unmuted; - - expect(actual, equals(expected)); - }); - }); - }); -} diff --git a/test/utils/speaker_state_test.dart b/test/utils/speaker_state_test.dart deleted file mode 100644 index 1e4ce10..0000000 --- a/test/utils/speaker_state_test.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:dieklingel_app/utils/speaker_state.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group("SpeakerState", () { - group(".next()", () { - test("without skip; muted to headphone", () { - SpeakerState state = SpeakerState.muted; - - SpeakerState actual = state.next(); - SpeakerState expected = SpeakerState.headphone; - - expect(actual, equals(expected)); - }); - - test("without skip; headphone to speaker", () { - SpeakerState state = SpeakerState.headphone; - - SpeakerState actual = state.next(); - SpeakerState expected = SpeakerState.speaker; - - expect(actual, equals(expected)); - }); - - test("without skip; speaker to muted", () { - SpeakerState state = SpeakerState.speaker; - - SpeakerState actual = state.next(); - SpeakerState expected = SpeakerState.muted; - - expect(actual, equals(expected)); - }); - - test("skip all; headphone to headphone", () { - SpeakerState state = SpeakerState.headphone; - - SpeakerState actual = state.next(skip: SpeakerState.values); - SpeakerState expected = SpeakerState.headphone; - - expect(actual, equals(expected)); - }); - - test("skip headphone; muted to speaker", () { - SpeakerState state = SpeakerState.muted; - - SpeakerState actual = state.next(skip: [SpeakerState.headphone]); - SpeakerState expected = SpeakerState.speaker; - - expect(actual, equals(expected)); - }); - - test("skip muted; speaker to headphone", () { - SpeakerState state = SpeakerState.speaker; - - SpeakerState actual = state.next(skip: [SpeakerState.muted]); - SpeakerState expected = SpeakerState.headphone; - - expect(actual, equals(expected)); - }); - }); - }); -}