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