diff --git a/.gitignore b/.gitignore index d5aa5f0..0eff3cf 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ migrate_working_dir/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id +**/ios/build/ .dart_tool/ .flutter-plugins .flutter-plugins-dependencies diff --git a/ios/Podfile b/ios/Podfile index 9a3af86..6546913 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '13.0' +platform :ios, '14.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -41,7 +41,7 @@ post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0' end end end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5924ee2..90bc870 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -5,35 +5,37 @@ PODS: - Flutter - camera_avfoundation (0.0.1): - Flutter - - DKImagePickerController/Core (4.3.4): + - device_info_plus (0.0.1): + - Flutter + - DKImagePickerController/Core (4.3.9): - DKImagePickerController/ImageDataManager - DKImagePickerController/Resource - - DKImagePickerController/ImageDataManager (4.3.4) - - DKImagePickerController/PhotoGallery (4.3.4): + - DKImagePickerController/ImageDataManager (4.3.9) + - DKImagePickerController/PhotoGallery (4.3.9): - DKImagePickerController/Core - DKPhotoGallery - - DKImagePickerController/Resource (4.3.4) - - DKPhotoGallery (0.0.17): - - DKPhotoGallery/Core (= 0.0.17) - - DKPhotoGallery/Model (= 0.0.17) - - DKPhotoGallery/Preview (= 0.0.17) - - DKPhotoGallery/Resource (= 0.0.17) + - DKImagePickerController/Resource (4.3.9) + - DKPhotoGallery (0.0.19): + - DKPhotoGallery/Core (= 0.0.19) + - DKPhotoGallery/Model (= 0.0.19) + - DKPhotoGallery/Preview (= 0.0.19) + - DKPhotoGallery/Resource (= 0.0.19) - SDWebImage - SwiftyGif - - DKPhotoGallery/Core (0.0.17): + - DKPhotoGallery/Core (0.0.19): - DKPhotoGallery/Model - DKPhotoGallery/Preview - SDWebImage - SwiftyGif - - DKPhotoGallery/Model (0.0.17): + - DKPhotoGallery/Model (0.0.19): - SDWebImage - SwiftyGif - - DKPhotoGallery/Preview (0.0.17): + - DKPhotoGallery/Preview (0.0.19): - DKPhotoGallery/Model - DKPhotoGallery/Resource - SDWebImage - SwiftyGif - - DKPhotoGallery/Resource (0.0.17): + - DKPhotoGallery/Resource (0.0.19): - SDWebImage - SwiftyGif - ffmpeg-kit-ios-audio (6.0) @@ -51,9 +53,9 @@ PODS: - Flutter - flutter_secure_storage (6.0.0): - Flutter - - FMDB (2.7.5): - - FMDB/standard (= 2.7.5) - - FMDB/standard (2.7.5) + - FMDB (2.7.11): + - FMDB/standard (= 2.7.11) + - FMDB/standard (2.7.11) - image_picker_ios (0.0.1): - Flutter - just_audio (0.0.1): @@ -68,15 +70,15 @@ PODS: - record_darwin (1.0.0): - Flutter - FlutterMacOS - - SDWebImage (5.13.2): - - SDWebImage/Core (= 5.13.2) - - SDWebImage/Core (5.13.2) + - SDWebImage (5.19.2): + - SDWebImage/Core (= 5.19.2) + - SDWebImage/Core (5.19.2) - share_plus (0.0.1): - Flutter - sqflite (0.0.3): - Flutter - FMDB (>= 2.7.5) - - SwiftyGif (5.4.4) + - SwiftyGif (5.4.5) - url_launcher_ios (0.0.1): - Flutter - workmanager (0.0.1): @@ -86,6 +88,7 @@ DEPENDENCIES: - audio_session (from `.symlinks/plugins/audio_session/ios`) - background_downloader (from `.symlinks/plugins/background_downloader/ios`) - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`) + - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - ffmpeg_kit_flutter_audio (from `.symlinks/plugins/ffmpeg_kit_flutter_audio/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) @@ -118,6 +121,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/background_downloader/ios" camera_avfoundation: :path: ".symlinks/plugins/camera_avfoundation/ios" + device_info_plus: + :path: ".symlinks/plugins/device_info_plus/ios" ffmpeg_kit_flutter_audio: :path: ".symlinks/plugins/ffmpeg_kit_flutter_audio/ios" file_picker: @@ -153,28 +158,29 @@ SPEC CHECKSUMS: audio_session: 4f3e461722055d21515cf3261b64c973c062f345 background_downloader: 6f55e5548875be2ad4bb91b542558b5be22f339a camera_avfoundation: 3125e8cd1a4387f6f31c6c63abb8a55892a9eeeb - DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac - DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 + device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 + DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c + DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 ffmpeg-kit-ios-audio: 9fa9953fc197280a69e59c603c7fa7690df7190c ffmpeg_kit_flutter_audio: 9b107d9902e16804c90637cd7f42106a5447a9e6 file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be - FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a + FMDB: 57486c1117fd8e0e6b947b2f54c3f42bf8e57a4e image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 just_audio: baa7252489dbcf47a4c7cc9ca663e9661c99aafa package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 record_darwin: 1f6619f2abac4d1ca91d3eeab038c980d76f1517 - SDWebImage: 72f86271a6f3139cc7e4a89220946489d4b9a866 + SDWebImage: dfe95b2466a9823cf9f0c6d01217c06550d7b29a share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a - SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f + SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 url_launcher_ios: 68d46cc9766d0c41dbdc884310529557e3cd7a86 workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 -PODFILE CHECKSUM: 0a7d5b7d0e53420cb0284f7b2f171f93843b94d2 +PODFILE CHECKSUM: de97cdd71230554f2f9175b64953d8f22054f2bf -COCOAPODS: 1.12.1 +COCOAPODS: 1.15.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 88f7ac5..5d53675 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -7,15 +7,15 @@ objects = { /* Begin PBXBuildFile section */ + 0C919EFF59BB245CE33C1729 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AD8D55317A9F9C1E8BDC309E /* Pods_RunnerTests.framework */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3EACF4502AF94B2E0009EB00 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3BAB80CE2FD566CD74754C6 /* Pods_Runner.framework */; }; + 3ECA41B22C2602CA000DDC0E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D3DE141E94870D9CA578104 /* Pods_Runner.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 */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - A04538A4FD002863EA8D7E2E /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B83C597EDF1CEFE95FFFB1B /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -31,18 +31,18 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 2A38106953F973A4A4D2889C /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 35345364120A3EBED9C200D8 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3D3DE141E94870D9CA578104 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3EACF44C2AF946870009EB00 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; 3EACF44D2AF94B1B0009EB00 /* sqflite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = sqflite.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 6357E70700B420135CF38106 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 4013FCB2867BE8C285FCE973 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 73E6CA98A4DD47389AC0DD2C /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 8B83C597EDF1CEFE95FFFB1B /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 947052A3147FEB296CDB1CF8 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* ReCon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ReCon.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -50,10 +50,10 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 98689629DBCBD9B9079D4BCB /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 9B70C4D26DEBAB78C4541963 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - ACF34F80AF1EDFE1E02822A3 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - C3BAB80CE2FD566CD74754C6 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + AD8D55317A9F9C1E8BDC309E /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + CA3447F4857A0510F7FFE6B7 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + D99312EB7B07F458E04BFDD3 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + DB366C7024A03BCB71242FB5 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -61,7 +61,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - A04538A4FD002863EA8D7E2E /* Pods_RunnerTests.framework in Frameworks */, + 0C919EFF59BB245CE33C1729 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -69,7 +69,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 3EACF4502AF94B2E0009EB00 /* Pods_Runner.framework in Frameworks */, + 3ECA41B22C2602CA000DDC0E /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -135,12 +135,12 @@ EB365C9671FE77D43024480F /* Pods */ = { isa = PBXGroup; children = ( - 98689629DBCBD9B9079D4BCB /* Pods-Runner.debug.xcconfig */, - ACF34F80AF1EDFE1E02822A3 /* Pods-Runner.release.xcconfig */, - 9B70C4D26DEBAB78C4541963 /* Pods-Runner.profile.xcconfig */, - 35345364120A3EBED9C200D8 /* Pods-RunnerTests.debug.xcconfig */, - 6357E70700B420135CF38106 /* Pods-RunnerTests.release.xcconfig */, - 947052A3147FEB296CDB1CF8 /* Pods-RunnerTests.profile.xcconfig */, + D99312EB7B07F458E04BFDD3 /* Pods-Runner.debug.xcconfig */, + 4013FCB2867BE8C285FCE973 /* Pods-Runner.release.xcconfig */, + CA3447F4857A0510F7FFE6B7 /* Pods-Runner.profile.xcconfig */, + 2A38106953F973A4A4D2889C /* Pods-RunnerTests.debug.xcconfig */, + 73E6CA98A4DD47389AC0DD2C /* Pods-RunnerTests.release.xcconfig */, + DB366C7024A03BCB71242FB5 /* Pods-RunnerTests.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -149,8 +149,8 @@ isa = PBXGroup; children = ( 3EACF44D2AF94B1B0009EB00 /* sqflite.framework */, - C3BAB80CE2FD566CD74754C6 /* Pods_Runner.framework */, - 8B83C597EDF1CEFE95FFFB1B /* Pods_RunnerTests.framework */, + 3D3DE141E94870D9CA578104 /* Pods_Runner.framework */, + AD8D55317A9F9C1E8BDC309E /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -162,7 +162,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 19ED0D0FE3A6C2191496F46B /* [CP] Check Pods Manifest.lock */, + 690A3B0A6C07C9F2F97916FF /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, 2597599605DD2CD4DB799735 /* Frameworks */, @@ -181,13 +181,13 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 197307D9FE00A90F03801302 /* [CP] Check Pods Manifest.lock */, + 8BF7047545265288073F8A43 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - BDF85620D00D0FE7A8BAEF7B /* [CP] Embed Pods Frameworks */, + 3E67E7F8ECF942FF5AF50545 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -259,29 +259,40 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 197307D9FE00A90F03801302 /* [CP] Check Pods Manifest.lock */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); + name = "Thin Binary"; outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-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"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 3E67E7F8ECF942FF5AF50545 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 19ED0D0FE3A6C2191496F46B /* [CP] Check Pods Manifest.lock */ = { + 690A3B0A6C07C9F2F97916FF /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -303,21 +314,27 @@ 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; }; - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + 8BF7047545265288073F8A43 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); - name = "Thin Binary"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + 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; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; @@ -334,23 +351,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - BDF85620D00D0FE7A8BAEF7B /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -404,7 +404,9 @@ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ALWAYS_SEARCH_USER_PATHS = NO; + APPLY_RULES_IN_COPY_HEADERS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -424,6 +426,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -435,6 +438,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -445,7 +449,12 @@ GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_KEY_CFBundleDisplayName = ReCon; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + INFOPLIST_KEY_NSDownloadsFolderUsageDescription = "Downloading assets from your inventory"; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "Recording voice messages"; + INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations = ""; + INFOPLIST_KEY_UIUserInterfaceStyle = Automatic; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; @@ -458,24 +467,26 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = P9AV4LPNLL; ENABLE_BITCODE = NO; - FLUTTER_BUILD_NAME = 0.10.3; + EXCLUDED_ARCHS = "$(inherited)"; + FLUTTER_BUILD_NAME = 0.11.2; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ReCon; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 0.10.3; - PRODUCT_BUNDLE_IDENTIFIER = me.voidspace.recon; + MARKETING_VERSION = 0.11.2; + PRODUCT_BUNDLE_IDENTIFIER = ch.isota.recon; PRODUCT_NAME = ReCon; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -485,7 +496,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 35345364120A3EBED9C200D8 /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = 2A38106953F973A4A4D2889C /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -503,7 +514,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 6357E70700B420135CF38106 /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = 73E6CA98A4DD47389AC0DD2C /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -519,7 +530,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 947052A3147FEB296CDB1CF8 /* Pods-RunnerTests.profile.xcconfig */; + baseConfigurationReference = DB366C7024A03BCB71242FB5 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -536,7 +547,9 @@ 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ALWAYS_SEARCH_USER_PATHS = NO; + APPLY_RULES_IN_COPY_HEADERS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -556,6 +569,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -567,6 +581,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -583,7 +598,12 @@ GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_KEY_CFBundleDisplayName = ReCon; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + INFOPLIST_KEY_NSDownloadsFolderUsageDescription = "Downloading assets from your inventory"; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "Recording voice messages"; + INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations = ""; + INFOPLIST_KEY_UIUserInterfaceStyle = Automatic; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -594,7 +614,9 @@ 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ALWAYS_SEARCH_USER_PATHS = NO; + APPLY_RULES_IN_COPY_HEADERS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -614,6 +636,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -625,6 +648,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -635,7 +659,12 @@ GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_KEY_CFBundleDisplayName = ReCon; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + INFOPLIST_KEY_NSDownloadsFolderUsageDescription = "Downloading assets from your inventory"; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "Recording voice messages"; + INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations = ""; + INFOPLIST_KEY_UIUserInterfaceStyle = Automatic; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; @@ -650,24 +679,26 @@ isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = P9AV4LPNLL; ENABLE_BITCODE = NO; - FLUTTER_BUILD_NAME = 0.10.3; + EXCLUDED_ARCHS = "$(inherited)"; + FLUTTER_BUILD_NAME = 0.11.2; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ReCon; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 0.10.3; - PRODUCT_BUNDLE_IDENTIFIER = me.voidspace.recon; + MARKETING_VERSION = 0.11.2; + PRODUCT_BUNDLE_IDENTIFIER = ch.isota.recon; PRODUCT_NAME = ReCon; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -680,24 +711,26 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = P9AV4LPNLL; ENABLE_BITCODE = NO; - FLUTTER_BUILD_NAME = 0.10.3; + EXCLUDED_ARCHS = "$(inherited)"; + FLUTTER_BUILD_NAME = 0.11.2; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ReCon; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 0.10.3; - PRODUCT_BUNDLE_IDENTIFIER = me.voidspace.recon; + MARKETING_VERSION = 0.11.2; + PRODUCT_BUNDLE_IDENTIFIER = ch.isota.recon; PRODUCT_NAME = ReCon; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 30e0c22..7002d47 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -52,8 +52,8 @@ - + + + - + + + @@ -10,16 +13,20 @@ - - + + - + + - + + + - + + @@ -28,10 +35,13 @@ - + - + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 7b71a45..656aed7 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,6 +2,10 @@ + BGTaskSchedulerPermittedIdentifiers + + ch.isota.recon + CADisableMinimumFrameDurationOnPhone CFBundleDevelopmentRegion @@ -26,13 +30,18 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + NSCameraUsageDescription + Send photos to other users + NSMicrophoneUsageDescription + ReCon uses your microphone to record voice messages. + NSPhotoLibraryUsageDescription + ReCon uses your photo library to send images to your contacts. UIApplicationSupportsIndirectInputEvents UIBackgroundModes fetch processing - remote-notification UILaunchStoryboardName LaunchScreen.storyboard @@ -41,15 +50,6 @@ UISupportedInterfaceOrientations UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index 39f5fef..3896ae5 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -2,9 +2,15 @@ + aps-environment + development + com.apple.developer.associated-domains + + webcredentials:resonite.com + keychain-access-groups - $(AppIdentifierPrefix)me.voidspace.recon + $(AppIdentifierPrefix)ch.isota.recon diff --git a/ios/Runner/en.lproj/LaunchScreen.strings b/ios/Runner/en.lproj/LaunchScreen.strings new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/ios/Runner/en.lproj/LaunchScreen.strings @@ -0,0 +1 @@ + diff --git a/lib/apis/user_api.dart b/lib/apis/user_api.dart index 5f29968..339f933 100644 --- a/lib/apis/user_api.dart +++ b/lib/apis/user_api.dart @@ -12,7 +12,7 @@ class UserApi { final data = jsonDecode(response.body) as List; return data.map((e) => User.fromMap(e)); } - + static Future getUser(ApiClient client, {required String userId}) async { final response = await client.get("/users/$userId/"); client.checkResponse(response); @@ -21,13 +21,12 @@ class UserApi { } static Future getUserStatus(ApiClient client, {required String userId}) async { - return UserStatus.empty(); final response = await client.get("/users/$userId/status"); client.checkResponse(response); final data = jsonDecode(response.body); return UserStatus.fromMap(data); } - + static Future notifyOnlineInstance(ApiClient client) async { final response = await client.post("/stats/instanceOnline/${client.authenticationData.secretMachineIdHash}"); client.checkResponse(response); @@ -46,4 +45,4 @@ class UserApi { final data = jsonDecode(response.body); return StorageQuota.fromMap(data); } -} \ No newline at end of file +} diff --git a/lib/auxiliary.dart b/lib/auxiliary.dart index 864f64c..7736d81 100644 --- a/lib/auxiliary.dart +++ b/lib/auxiliary.dart @@ -1,7 +1,7 @@ -import 'package:recon/config.dart'; import 'package:flutter/material.dart'; -import 'package:path/path.dart' as p; import 'package:html/parser.dart' as htmlparser; +import 'package:path/path.dart' as p; +import 'package:recon/config.dart'; class Aux { static String resdbToHttp(String? resdb) { @@ -60,4 +60,8 @@ extension ColorX on Color { return Color.fromARGB((opacity * 255).round(), r, g, b); } -} \ No newline at end of file + + String toHex() => value.toRadixString(16).substring(2); + + String toCss() => "#${toHex()}"; +} diff --git a/lib/clients/audio_cache_client.dart b/lib/clients/audio_cache_client.dart index 619b0ab..0b7e880 100644 --- a/lib/clients/audio_cache_client.dart +++ b/lib/clients/audio_cache_client.dart @@ -8,6 +8,7 @@ import 'package:recon/auxiliary.dart'; import 'package:recon/clients/api_client.dart'; import 'package:recon/models/message.dart'; +// TODO: Don't use wav, use vorbis maybe? class AudioCacheClient { final Future _directoryFuture = getTemporaryDirectory(); final bool _isDarwin = Platform.isMacOS || Platform.isIOS; diff --git a/lib/color_palette.dart b/lib/color_palette.dart new file mode 100644 index 0000000..14cfaaa --- /dev/null +++ b/lib/color_palette.dart @@ -0,0 +1,181 @@ +import 'dart:ui'; + +class NeutralColors { + const NeutralColors({required this.dark, required this.mid, required this.light}); + + final Color dark; + final Color mid; + final Color light; + + Color operator [](String key) { + switch (key) { + case "dark": + return dark; + case "mid": + return mid; + case "light": + return light; + default: + return mid; + } + } + + Color getColor(String? key) { + if (key == null) return mid; + switch (key) { + case "dark": + return dark; + case "mid": + return mid; + case "light": + return light; + default: + return mid; + } + } + + Map toMap() { + return { + "dark": dark, + "mid": mid, + "light": light, + }; + } +} + +class ColorSet { + const ColorSet({ + required this.yellow, + required this.green, + required this.red, + required this.purple, + required this.cyan, + required this.orange, + }); + + final Color yellow; + final Color green; + final Color red; + final Color purple; + final Color cyan; + final Color orange; + + Color operator [](String key) { + switch (key) { + case "yellow": + return yellow; + case "green": + return green; + case "red": + return red; + case "purple": + return purple; + case "cyan": + return cyan; + case "orange": + return orange; + default: + return yellow; + } + } + + Color getColor(String? key) { + if (key == null) return yellow; + switch (key) { + case "yellow": + return yellow; + case "green": + return green; + case "red": + return red; + case "purple": + return purple; + case "cyan": + return cyan; + case "orange": + return orange; + default: + return yellow; + } + } + + Map toMap() { + return { + "yellow": yellow, + "green": green, + "red": red, + "purple": purple, + "cyan": cyan, + "orange": orange, + }; + } +} + +class ColorPalette { + const ColorPalette({ + required this.neutrals, + required this.hero, + required this.mid, + required this.sub, + required this.dark, + }); + + final NeutralColors neutrals; + final ColorSet hero; + final ColorSet mid; + final ColorSet sub; + final ColorSet dark; + + Map toMap() { + return { + "neutrals": neutrals.toMap(), + "hero": hero.toMap(), + "mid": mid.toMap(), + "sub": sub.toMap(), + "dark": dark.toMap(), + }.entries.where((e) => e.key != "mid").fold( + {}, + (acc, e) => acc..addAll(e.value.map((key, value) => MapEntry("${e.key}.$key", value))), + ); + } +} + +const ColorPalette palette = ColorPalette( + neutrals: NeutralColors( + dark: Color(0xFF11151D), + mid: Color(0xFF86888B), + light: Color(0xFFE1E1E0), + ), + hero: ColorSet( + yellow: Color(0xFFF8F770), + green: Color(0xFF59EB5C), + red: Color(0xFFFF7676), + purple: Color(0xFFBA64F2), + cyan: Color(0xFF61D1FA), + orange: Color(0xFFE69E50), + ), + mid: ColorSet( + yellow: Color(0xFFA0A14E), + green: Color(0xFF3F9E44), + red: Color(0xFFAE5458), + purple: Color(0xFF824AAB), + cyan: Color(0xFF458FAB), + orange: Color(0xFF976C3D), + ), + sub: ColorSet( + yellow: Color(0xFF484A2C), + green: Color(0xFF24512C), + red: Color(0xFF5D323A), + purple: Color(0xFF492F64), + cyan: Color(0xFF284C5D), + orange: Color(0xFF48392A), + ), + dark: ColorSet( + yellow: Color(0xFF2B2E26), + green: Color(0xFF192D24), + red: Color(0xFF1A1318), + purple: Color(0xFF241E35), + cyan: Color(0xFF1A2A36), + orange: Color(0xFF292423), + ), +); diff --git a/lib/device_info.dart b/lib/device_info.dart new file mode 100644 index 0000000..1d5b48e --- /dev/null +++ b/lib/device_info.dart @@ -0,0 +1,94 @@ +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +/// Information about the device running the app. +class DeviceInfo { + static final RegExp _r39 = RegExp(r'iPhone(10,(3|6)|11,(2|4|6)|12,(3|5))'); + static final RegExp _r41 = RegExp(r'iPhone(11,8|12,1)'); + static final RegExp _r44 = RegExp(r'iPhone(13,1|14,4)'); + static final RegExp _r47 = RegExp(r'iPhone(13,(2|3)|14,(2|5|7))'); + static final RegExp _r53 = RegExp(r'iPhone(13,4|14,(3|8))'); + static final RegExp _r55 = RegExp(r'iPhone(15,(2|3|4|5)|16,(1|2))'); + + static final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + + static BaseDeviceInfo? _info; + static WindowsDeviceInfo? _windowsInfo; + static MacOsDeviceInfo? _macOsInfo; + static LinuxDeviceInfo? _linuxInfo; + static IosDeviceInfo? _iosInfo; + static AndroidDeviceInfo? _androidInfo; + static Radius? _bezelRadius; + + /// The device information for the device running the app. + static BaseDeviceInfo? get info => _info; + + /// The device information for Windows devices. + /// + /// This will be `null` if the app is not running on a Windows device. + static WindowsDeviceInfo? get windowsInfo => _windowsInfo; + + /// The device information for macOS devices. + /// + /// This will be `null` if the app is not running on a macOS device. + static MacOsDeviceInfo? get macOsInfo => _macOsInfo; + + /// The device information for Linux devices. + /// + /// This will be `null` if the app is not running on a Linux device. + static LinuxDeviceInfo? get linuxInfo => _linuxInfo; + + /// The device information for iOS devices. + /// + /// This will be `null` if the app is not running on an iOS device. + static IosDeviceInfo? get iosInfo => _iosInfo; + + /// The device information for Android devices. + /// + /// This will be `null` if the app is not running on an Android device. + static AndroidDeviceInfo? get androidInfo => _androidInfo; + + /// The radius of the corners of the device's screen bezel (if applicable). + /// + /// This will be `null` if the app is running on a device with a rectangular screen. + static Radius? get bezelRadius => _bezelRadius; + + /// Initializes device information. This method should be called before the app is run. + static Future initDeviceInfo() async { + _info = await deviceInfo.iosInfo; + + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + _iosInfo = await deviceInfo.iosInfo; + + try { + final String model = _iosInfo!.utsname.machine; + if (_r39.hasMatch(model)) _bezelRadius = const Radius.circular(39); + if (_r41.hasMatch(model)) _bezelRadius = const Radius.circular(41); + if (_r44.hasMatch(model)) _bezelRadius = const Radius.circular(44); + if (_r47.hasMatch(model)) _bezelRadius = const Radius.circular(47); + if (_r53.hasMatch(model)) _bezelRadius = const Radius.circular(53); + if (_r55.hasMatch(model)) _bezelRadius = const Radius.circular(55); + } catch (e) { + return; + } + break; + case TargetPlatform.android: + _androidInfo = await deviceInfo.androidInfo; + break; + case TargetPlatform.windows: + _windowsInfo = await deviceInfo.windowsInfo; + break; + case TargetPlatform.macOS: + _macOsInfo = await deviceInfo.macOsInfo; + break; + case TargetPlatform.linux: + _linuxInfo = await deviceInfo.linuxInfo; + break; + case TargetPlatform.fuchsia: + break; + } + } +} diff --git a/lib/main.dart b/lib/main.dart index 897c1a2..8cf75e2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,6 +20,8 @@ import 'package:recon/clients/inventory_client.dart'; import 'package:recon/clients/messaging_client.dart'; import 'package:recon/clients/session_client.dart'; import 'package:recon/clients/settings_client.dart'; +import 'package:recon/color_palette.dart'; +import 'package:recon/device_info.dart'; import 'package:recon/models/sem_ver.dart'; import 'package:recon/widgets/homepage.dart'; import 'package:recon/widgets/login_screen.dart'; @@ -40,6 +42,8 @@ void main() async { SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge, overlays: [SystemUiOverlay.top]); + await DeviceInfo.initDeviceInfo(); + await Hive.initFlutter(); final dateFormat = DateFormat.Hms(); @@ -169,13 +173,38 @@ class _ReConState extends State { theme: ThemeData( useMaterial3: true, textTheme: _typography.black, - colorScheme: - lightDynamic ?? ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.light), + colorScheme: lightDynamic ?? + ColorScheme.fromSeed( + brightness: Brightness.light, + seedColor: palette.hero.cyan, + primary: palette.dark.cyan, + secondary: palette.mid.cyan, + tertiary: palette.sub.cyan, + surface: palette.hero.cyan.withOpacity(0.5), + onSurface: palette.neutrals.dark, + background: palette.neutrals.light, + onBackground: palette.neutrals.dark, + outline: palette.neutrals.dark.withOpacity(0.1), + outlineVariant: palette.neutrals.dark.withOpacity(0.2), + ), ), darkTheme: ThemeData( useMaterial3: true, textTheme: _typography.white, - colorScheme: darkDynamic ?? ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark), + colorScheme: darkDynamic ?? + ColorScheme.fromSeed( + brightness: Brightness.dark, + seedColor: palette.hero.cyan, + primary: palette.sub.cyan, + secondary: palette.hero.cyan, + tertiary: palette.sub.cyan, + surface: palette.dark.cyan.withOpacity(0.5), + onSurface: palette.neutrals.light, + background: palette.neutrals.dark, + onBackground: palette.neutrals.light, + outline: palette.neutrals.light.withOpacity(0.1), + outlineVariant: palette.neutrals.light.withOpacity(0.2), + ), ), themeMode: ThemeMode.values[widget.settingsClient.currentSettings.themeMode.valueOrDefault], home: Builder( diff --git a/lib/models/personal_profile.dart b/lib/models/personal_profile.dart index 7443875..04af217 100644 --- a/lib/models/personal_profile.dart +++ b/lib/models/personal_profile.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:recon/auxiliary.dart'; import 'package:recon/models/users/entitlement.dart'; import 'package:recon/models/users/user_profile.dart'; @@ -9,6 +10,7 @@ class PersonalProfile { final DateTime? publicBanExpiration; final String? publicBanType; final bool twoFactor; + final List tags; final UserProfile userProfile; final List entitlements; final List supporterMetadata; @@ -20,6 +22,7 @@ class PersonalProfile { required this.publicBanExpiration, required this.publicBanType, required this.twoFactor, + required this.tags, required this.userProfile, required this.entitlements, required this.supporterMetadata, @@ -33,6 +36,7 @@ class PersonalProfile { publicBanExpiration: DateTime.tryParse(map["publicBanExpiration"] ?? ""), publicBanType: map["publicBanType"], twoFactor: map["2fa_login"] ?? false, + tags: (map["tags"] ?? []).cast(), userProfile: UserProfile.fromMap(map["profile"]), entitlements: ((map["entitlements"] ?? []) as List).map((e) => Entitlement.fromMap(e)).toList(), supporterMetadata: ((map["supporterMetadata"] ?? []) as List).map((e) => SupporterMetadata.fromMap(e)).toList(), @@ -41,6 +45,26 @@ class PersonalProfile { bool get isPatreonSupporter => supporterMetadata.whereType().any((element) => element.isActiveSupporter); + + AccountType get accountType => tags.contains("team member") + ? AccountType(label: "Resonite Team", color: const Color.fromARGB(255, 255, 230, 0)) + : tags.contains("moderator") + ? AccountType(label: "Resonite Moderator", color: const Color(0xFF61D1FA)) + : tags.contains("mentor") + ? AccountType(label: "Resonite Mentor", color: const Color(0xFF59EB5C)) + : isPatreonSupporter + ? AccountType(label: "Patreon Supporter", color: const Color(0xFFFF7676)) + : AccountType(label: "Standard Account", color: const Color(0xFF86888B)); +} + +class AccountType { + final String label; + final Color color; + + AccountType({ + required this.label, + required this.color, + }); } class StorageQuota { diff --git a/lib/models/records/asset_diff.dart b/lib/models/records/asset_diff.dart index 5e590c5..16d4474 100644 --- a/lib/models/records/asset_diff.dart +++ b/lib/models/records/asset_diff.dart @@ -1,11 +1,10 @@ - import 'package:recon/models/records/resonite_db_asset.dart'; -class AssetDiff extends ResoniteDBAsset{ +class AssetDiff extends ResoniteDBAsset { final Diff state; final bool isUploaded; - const AssetDiff({required hash, required bytes, required this.state, required this.isUploaded}) : super(hash: hash, bytes: bytes); + const AssetDiff({required super.hash, required super.bytes, required this.state, required this.isUploaded}); factory AssetDiff.fromMap(Map map) { return AssetDiff( @@ -27,8 +26,9 @@ enum Diff { } factory Diff.fromString(String? text) { - return Diff.values.firstWhere((element) => element.name.toLowerCase() == text?.toLowerCase(), + return Diff.values.firstWhere( + (element) => element.name.toLowerCase() == text?.toLowerCase(), orElse: () => Diff.unchanged, ); } -} \ No newline at end of file +} diff --git a/lib/models/session.dart b/lib/models/session.dart index 1bc2791..e9e188d 100644 --- a/lib/models/session.dart +++ b/lib/models/session.dart @@ -139,9 +139,9 @@ enum SessionAccessLevel { static const _readableNamesMap = { SessionAccessLevel.unknown: "Unknown", SessionAccessLevel.private: "Private", - SessionAccessLevel.contacts: "Contacts only", + SessionAccessLevel.contacts: "Contacts Only", SessionAccessLevel.contactsPlus: "Contacts+", - SessionAccessLevel.registeredUsers: "Registered users", + SessionAccessLevel.registeredUsers: "Registered Users", SessionAccessLevel.anyone: "Public", }; diff --git a/lib/models/users/online_status.dart b/lib/models/users/online_status.dart index 2d8d8bf..31f9094 100644 --- a/lib/models/users/online_status.dart +++ b/lib/models/users/online_status.dart @@ -1,18 +1,21 @@ import 'package:flutter/material.dart'; +import 'package:recon/color_palette.dart'; enum OnlineStatus { offline, invisible, away, busy, - online; + online, + social; static final List _colors = [ Colors.transparent, Colors.transparent, - Colors.yellow, - Colors.red, - Colors.green, + palette.hero.yellow, + palette.hero.red, + palette.hero.green, + palette.hero.cyan, ]; Color color(BuildContext context) => this == OnlineStatus.offline || this == OnlineStatus.invisible @@ -28,6 +31,8 @@ enum OnlineStatus { int compareTo(OnlineStatus other) { if (this == other) return 0; + if (this == OnlineStatus.social) return -1; + if (other == OnlineStatus.social) return 1; if (this == OnlineStatus.online) return -1; if (other == OnlineStatus.online) return 1; if (this == OnlineStatus.away) return -1; diff --git a/lib/models/users/user_profile.dart b/lib/models/users/user_profile.dart index f78504a..edfee61 100644 --- a/lib/models/users/user_profile.dart +++ b/lib/models/users/user_profile.dart @@ -1,17 +1,38 @@ class UserProfile { final String iconUrl; + final String tagline; + final String description; + final List displayBadges; - UserProfile({required this.iconUrl}); + UserProfile({ + required this.iconUrl, + required this.tagline, + required this.description, + required this.displayBadges, + }); - factory UserProfile.empty() => UserProfile(iconUrl: ""); + factory UserProfile.empty() => UserProfile( + iconUrl: "", + tagline: "", + description: "", + displayBadges: [], + ); factory UserProfile.fromMap(Map? map) { - return UserProfile(iconUrl: map?["iconUrl"] ?? ""); + return UserProfile( + iconUrl: map?["iconUrl"] ?? "", + tagline: map?["tagline"] ?? "", + description: map?["description"] ?? "", + displayBadges: map?["displayBadges"]?.cast() ?? [], + ); } Map toMap() { return { "iconUrl": iconUrl, + "tagline": tagline, + "description": description, + "displayBadges": displayBadges, }; } -} \ No newline at end of file +} diff --git a/lib/string_formatter.dart b/lib/string_formatter.dart index 2d76639..5cdf990 100644 --- a/lib/string_formatter.dart +++ b/lib/string_formatter.dart @@ -1,5 +1,6 @@ import 'package:color/color.dart' as cc; import 'package:flutter/material.dart'; +import 'package:recon/color_palette.dart'; class FormatNode { String text; @@ -72,6 +73,16 @@ class FormatNode { current.text = text; return root; } + + static FormatNode merge(List nodes) { + if (nodes.isEmpty) return FormatNode.unformatted(""); + final root = FormatNode( + text: '', + format: FormatData.unformatted(), + children: nodes, + ); + return root; + } } class FormatTag { @@ -114,38 +125,7 @@ class FormatAction { } class FormatData { - static final Map> _platformColorPalette = { - "neutrals": { - "dark": const Color(0xFF11151D), - "mid": const Color(0xFF86888B), - "light": const Color(0xFFE1E1E0), - }, - "hero": { - "yellow": const Color(0xFFF8F770), - "green": const Color(0xFF59EB5C), - "red": const Color(0xFFFF7676), - "purple": const Color(0xFFBA64F2), - "cyan": const Color(0xFF61D1FA), - "orange": const Color(0xFFE69E50), - }, - "sub": { - "yellow": const Color(0xFF484A2C), - "green": const Color(0xFF24512C), - "red": const Color(0xFF5D323A), - "purple": const Color(0xFF492F64), - "cyan": const Color(0xFF284C5D), - "orange": const Color(0xFF48392A), - }, - "dark": { - "yellow": const Color(0xFF2B2E26), - "green": const Color(0xFF192D24), - "red": const Color(0xFF1A1318), - "purple": const Color(0xFF241E35), - "cyan": const Color(0xFF1A2A36), - "orange": const Color(0xFF292423), - }, - }; - + static final Map _platformColorPalette = palette.toMap(); static final _hexColorRegExp = RegExp(r"^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"); static final _platformColorRegExp = RegExp(r"^([a-zA-Z]+)\.([a-zA-Z]+)$"); @@ -168,13 +148,7 @@ class FormatData { // is it one of Resonite's color constants? if (_platformColorRegExp.hasMatch(text)) { - final parts = text.split("."); - if (parts.length == 2) { - final palette = _platformColorPalette[parts[0]]; - if (palette != null) { - return palette[parts[1]]; - } - } + return _platformColorPalette[text]; } // is it a named color? @@ -254,8 +228,13 @@ class FormatData { "width": FormatAction(), }; + /// The name of the format tag (e.g. `"color"`, `"b"`, `"i"`) final String name; + + /// The value of the format tag (e.g. `"#FF0000"`, `"bold"`) final String parameter; + + /// Whether the format tag is additive or not final bool isAdditive; const FormatData({required this.name, required this.parameter, required this.isAdditive}); diff --git a/lib/widgets/blend_mask.dart b/lib/widgets/blend_mask.dart new file mode 100644 index 0000000..43393ea --- /dev/null +++ b/lib/widgets/blend_mask.dart @@ -0,0 +1,49 @@ +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +// Applies a BlendMode to its child. +class BlendMask extends SingleChildRenderObjectWidget { + final BlendMode blendMode; + final double opacity; + + const BlendMask({ + required this.blendMode, + this.opacity = 1.0, + super.key, + super.child, + }); + + @override + RenderObject createRenderObject(context) { + return RenderBlendMask(blendMode, opacity); + } + + @override + void updateRenderObject(BuildContext context, RenderBlendMask renderObject) { + renderObject.blendMode = blendMode; + renderObject.opacity = opacity; + } +} + +class RenderBlendMask extends RenderProxyBox { + BlendMode blendMode; + double opacity; + + RenderBlendMask(this.blendMode, this.opacity); + + @override + void paint(context, offset) { + // Create a new layer and specify the blend mode and opacity to composite it with: + context.canvas.saveLayer( + offset & size, + Paint() + ..blendMode = blendMode + ..color = Color.fromARGB((opacity * 255).round(), 255, 255, 255), + ); + + super.paint(context, offset); + + // Composite the layer back into the canvas using the blendmode: + context.canvas.restore(); + } +} diff --git a/lib/widgets/friends/friend_list_tile.dart b/lib/widgets/friends/friend_list_tile.dart index 36dc4e4..32f5e2e 100644 --- a/lib/widgets/friends/friend_list_tile.dart +++ b/lib/widgets/friends/friend_list_tile.dart @@ -6,6 +6,7 @@ import 'package:recon/clients/messaging_client.dart'; import 'package:recon/models/message.dart'; import 'package:recon/models/users/friend.dart'; import 'package:recon/models/users/online_status.dart'; +import 'package:recon/string_formatter.dart'; import 'package:recon/widgets/formatted_text.dart'; import 'package:recon/widgets/friends/friend_online_status_indicator.dart'; import 'package:recon/widgets/generic_avatar.dart'; @@ -26,6 +27,40 @@ class FriendListTile extends StatelessWidget { final currentSession = friend.userStatus.currentSessionIndex == -1 ? null : friend.userStatus.decodedSessions.elementAtOrNull(friend.userStatus.currentSessionIndex); + + FormatNode offlineStatus = FormatNode.buildFromStyles( + [FormatData(name: 'color', parameter: OnlineStatus.offline.color(context).toCss(), isAdditive: true)], + 'Offline', + ); + FormatNode headlessHostStatus = FormatNode.buildFromStyles( + [FormatData(name: 'color', parameter: const Color.fromARGB(255, 41, 77, 92).toCss(), isAdditive: true)], + 'Headless Host', + ); + + List statusSegments = [ + FormatNode.unformatted(toBeginningOfSentenceCase(friend.userStatus.onlineStatus.name) ?? '') + ]; + + if (friend.isOffline) { + statusSegments = [offlineStatus]; + } else if (friend.isHeadless) { + statusSegments = [headlessHostStatus]; + } else if (currentSession != null) { + statusSegments.add(FormatNode.unformatted(' in ')); + if (currentSession.name.isNotEmpty) { + statusSegments.add(currentSession.formattedName); + } else { + final bool showHidden = !currentSession.isVisible && currentSession.accessLevel.index != 1; + final bool hideAccessLevel = currentSession.accessLevel.index > 3 || currentSession.accessLevel.index == 0; + statusSegments.add(FormatNode.unformatted( + 'a ${showHidden ? 'Hidden${hideAccessLevel ? '' : ', '}' : ''}${!hideAccessLevel ? currentSession.accessLevel.toReadableString() : ''} World')); + } + } else if (friend.userStatus.appVersion.isNotEmpty) { + statusSegments.add(FormatNode.unformatted(' on version ${friend.userStatus.appVersion}')); + } + + FormatNode formattedStatus = FormatNode.merge(statusSegments); + return ListTile( leading: GenericAvatar( imageUri: imageUri, @@ -54,56 +89,15 @@ class FriendListTile extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - FriendOnlineStatusIndicator(friend: friend), - const SizedBox( - width: 4, - ), - if (!(friend.isOffline || friend.isHeadless)) ...[ - Text(toBeginningOfSentenceCase(friend.userStatus.onlineStatus.name) ?? "Unknown"), - if (currentSession != null) ...[ - const Text(" in "), - if (currentSession.name.isNotEmpty) - Expanded( - child: FormattedText( - currentSession.formattedName, - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ) - else - Expanded( - child: Text( - "${currentSession.accessLevel.toReadableString()} World", - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ) - ] else if (friend.userStatus.appVersion.isNotEmpty) - Expanded( - child: Text( - " on version ${friend.userStatus.appVersion}", - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ), - ] else if (friend.isOffline) - Text( - "Offline", + FriendOnlineStatusIndicator(friend), + const SizedBox(width: 4), + Expanded( + child: FormattedText( + formattedStatus, overflow: TextOverflow.ellipsis, maxLines: 1, - style: theme.textTheme.bodyMedium?.copyWith( - color: OnlineStatus.offline.color(context), - ), - ) - else - Text( - "Headless Host", - overflow: TextOverflow.ellipsis, - maxLines: 1, - style: theme.textTheme.bodyMedium?.copyWith( - color: const Color.fromARGB(255, 41, 77, 92), - ), - ) + ), + ), ], ), onTap: () async { diff --git a/lib/widgets/friends/friend_online_status_indicator.dart b/lib/widgets/friends/friend_online_status_indicator.dart index 65e4d58..7b3e76f 100644 --- a/lib/widgets/friends/friend_online_status_indicator.dart +++ b/lib/widgets/friends/friend_online_status_indicator.dart @@ -4,7 +4,7 @@ import 'package:recon/models/users/online_status.dart'; import 'package:recon/models/users/user_status.dart'; class FriendOnlineStatusIndicator extends StatelessWidget { - const FriendOnlineStatusIndicator({required this.friend, super.key}); + const FriendOnlineStatusIndicator(this.friend, {super.key}); final Friend friend; diff --git a/lib/widgets/friends/friends_list.dart b/lib/widgets/friends/friends_list.dart index 3fd2430..01e0eee 100644 --- a/lib/widgets/friends/friends_list.dart +++ b/lib/widgets/friends/friends_list.dart @@ -1,9 +1,8 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:recon/clients/messaging_client.dart'; import 'package:recon/widgets/default_error_widget.dart'; -import 'package:recon/widgets/friends/expanding_input_fab.dart'; import 'package:recon/widgets/friends/friend_list_tile.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; class FriendsList extends StatefulWidget { const FriendsList({super.key}); @@ -17,71 +16,54 @@ class _FriendsListState extends State with AutomaticKeepAliveClient @override Widget build(BuildContext context) { + ThemeData theme = Theme.of(context); + super.build(context); return Consumer( builder: (context, mClient, _) { - return Stack( - alignment: Alignment.topCenter, - children: [ - Builder( - builder: (context) { - if (mClient.initStatus == null) { - return const LinearProgressIndicator(); - } else if (mClient.initStatus!.isNotEmpty) { - return Column( - children: [ - Expanded( - child: DefaultErrorWidget( - message: mClient.initStatus, - onRetry: () async { - mClient.resetInitStatus(); - mClient.refreshFriendsListWithErrorHandler(); - }, - ), - ), - ], - ); - } else { - var friends = List.from(mClient.cachedFriends); // Explicit copy. - if (_searchFilter.isNotEmpty) { - friends = friends - .where((element) => element.username.toLowerCase().contains(_searchFilter.toLowerCase())) - .toList(); - friends.sort((a, b) => a.username.length.compareTo(b.username.length)); - } - return ListView.builder( - physics: const BouncingScrollPhysics(decelerationRate: ScrollDecelerationRate.fast), - itemCount: friends.length, - itemBuilder: (context, index) { - final friend = friends[index]; - final unreads = mClient.getUnreadsForFriend(friend); - return FriendListTile( - friend: friend, - unreads: unreads.length, - ); - }, + return Builder( + builder: (context) { + if (mClient.initStatus == null) { + return LinearProgressIndicator( + color: theme.colorScheme.surface, + backgroundColor: theme.colorScheme.background, + ); + } else if (mClient.initStatus!.isNotEmpty) { + return Column( + children: [ + Expanded( + child: DefaultErrorWidget( + message: mClient.initStatus, + onRetry: () async { + mClient.resetInitStatus(); + mClient.refreshFriendsListWithErrorHandler(); + }, + ), + ), + ], + ); + } else { + var friends = List.from(mClient.cachedFriends); // Explicit copy. + if (_searchFilter.isNotEmpty) { + friends = friends + .where((element) => element.username.toLowerCase().contains(_searchFilter.toLowerCase())) + .toList(); + friends.sort((a, b) => a.username.length.compareTo(b.username.length)); + } + return ListView.builder( + physics: const BouncingScrollPhysics(decelerationRate: ScrollDecelerationRate.fast), + itemCount: friends.length, + itemBuilder: (context, index) { + final friend = friends[index]; + final unreads = mClient.getUnreadsForFriend(friend); + return FriendListTile( + friend: friend, + unreads: unreads.length, ); - } - }, - ), - Align( - alignment: Alignment.bottomCenter, - child: ExpandingInputFab( - onInputChanged: (String text) { - setState(() { - _searchFilter = text; - }); - }, - onExpansionChanged: (expanded) { - if (!expanded) { - setState(() { - _searchFilter = ""; - }); - } }, - ), - ), - ], + ); + } + }, ); }, ); diff --git a/lib/widgets/friends/friends_list_app_bar.dart b/lib/widgets/friends/friends_list_app_bar.dart index fbee120..ba116ee 100644 --- a/lib/widgets/friends/friends_list_app_bar.dart +++ b/lib/widgets/friends/friends_list_app_bar.dart @@ -1,12 +1,12 @@ import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; import 'package:recon/client_holder.dart'; import 'package:recon/clients/messaging_client.dart'; import 'package:recon/models/users/online_status.dart'; import 'package:recon/widgets/friends/user_search.dart'; import 'package:recon/widgets/my_profile_dialog.dart'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:provider/provider.dart'; class FriendsListAppBar extends StatefulWidget { const FriendsListAppBar({super.key}); @@ -16,12 +16,26 @@ class FriendsListAppBar extends StatefulWidget { } class _FriendsListAppBarState extends State with AutomaticKeepAliveClientMixin { - @override Widget build(BuildContext context) { super.build(context); return AppBar( - title: const Text("ReCon"), + title: Row( + children: [ + SizedBox.square( + dimension: 28, + child: Image.asset( + "assets/images/logo.png", + filterQuality: FilterQuality.medium, + isAntiAlias: true, + ), + ), + const SizedBox(width: 8), + const Text("ReCon") + ], + ), + centerTitle: false, + backgroundColor: Colors.transparent, actions: [ Consumer(builder: (context, client, _) { return PopupMenuButton( @@ -53,7 +67,10 @@ class _FriendsListAppBarState extends State with AutomaticKee } }, itemBuilder: (BuildContext context) => OnlineStatus.values - .where((element) => element == OnlineStatus.online || element == OnlineStatus.offline).sorted((a, b) => b.index.compareTo(a.index),) + .where((element) => element == OnlineStatus.online || element == OnlineStatus.offline) + .sorted( + (a, b) => b.index.compareTo(a.index), + ) .map( (item) => PopupMenuItem( value: item, diff --git a/lib/widgets/homepage.dart b/lib/widgets/homepage.dart index 2cbece2..d5159fa 100644 --- a/lib/widgets/homepage.dart +++ b/lib/widgets/homepage.dart @@ -3,10 +3,12 @@ import 'package:recon/widgets/friends/friends_list.dart'; import 'package:recon/widgets/friends/friends_list_app_bar.dart'; import 'package:recon/widgets/inventory/inventory_browser.dart'; import 'package:recon/widgets/inventory/inventory_browser_app_bar.dart'; +import 'package:recon/widgets/recon_navbar.dart'; import 'package:recon/widgets/sessions/session_list.dart'; import 'package:recon/widgets/sessions/session_list_app_bar.dart'; import 'package:recon/widgets/settings_app_bar.dart'; import 'package:recon/widgets/settings_page.dart'; +import 'package:recon/widgets/translucent_glass.dart'; class Home extends StatefulWidget { const Home({super.key}); @@ -15,69 +17,95 @@ class Home extends StatefulWidget { State createState() => _HomeState(); } -class _HomeState extends State { +class _HomeState extends State with SingleTickerProviderStateMixin { static const List _appBars = [ FriendsListAppBar(), SessionListAppBar(), InventoryBrowserAppBar(), + SettingsAppBar(), SettingsAppBar() ]; + + static const List navigationDestinations = [ + ReConNavigationDestination(label: 'Contacts', icon: Icon(Icons.mail_outline_rounded), color: 'green'), + ReConNavigationDestination(label: 'Sessions', icon: Icon(Icons.public), color: 'cyan'), + ReConNavigationDestination(label: 'Inventory', icon: Icon(Icons.backpack_outlined), color: 'yellow'), + ReConNavigationDestination(label: 'Mods', icon: Icon(Icons.settings), color: 'red'), + ReConNavigationDestination(label: 'Settings', icon: Icon(Icons.settings), color: 'purple'), + ]; + final PageController _pageController = PageController(); int _selectedPage = 0; + late AnimationController animationController; + + @override + void initState() { + super.initState(); + animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 100), + reverseDuration: const Duration(milliseconds: 50), + ); + } + + @override + void dispose() { + animationController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + return Scaffold( - backgroundColor: Theme.of(context).colorScheme.surface, - appBar: PreferredSize( - preferredSize: const Size.fromHeight(kToolbarHeight), - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - child: _appBars[_selectedPage], - ), - ), - body: PageView( - controller: _pageController, - physics: const NeverScrollableScrollPhysics(), - children: const [ - FriendsList(), - SessionList(), - InventoryBrowser(), - SettingsPage(), - ], - ), - bottomNavigationBar: NavigationBar( - selectedIndex: _selectedPage, - onDestinationSelected: (index) { - _pageController.animateToPage( - index, - duration: const Duration(milliseconds: 200), - curve: Curves.easeOut, - ); - setState(() { - _selectedPage = index; - }); - }, - destinations: const [ - NavigationDestination( - icon: Icon(Icons.message), - label: "Chat", - ), - NavigationDestination( - icon: Icon(Icons.public), - label: "Sessions", - ), - NavigationDestination( - icon: Icon(Icons.inventory), - label: "Inventory", + backgroundColor: theme.colorScheme.background, + extendBodyBehindAppBar: true, + extendBody: true, + appBar: PreferredSize( + preferredSize: const Size.fromHeight(kToolbarHeight), + child: TranslucentGlass.edgeToEdge( + context, + gradient: TranslucentGlass.defaultTopGradient(context), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: _appBars[_selectedPage], + ), ), - NavigationDestination( - icon: Icon(Icons.settings), - label: "Settings", - ), - ], - ), - ); + ), + body: PageView( + controller: _pageController, + physics: const NeverScrollableScrollPhysics(), + children: const [ + FriendsList(), + SessionList(), + InventoryBrowser(), + SettingsPage(), + SettingsPage(), + ], + ), + bottomNavigationBar: ReConNavigationBar( + animationController: animationController, + selectedIndex: _selectedPage, + onDestinationSelected: (int index) async { + if (_selectedPage != index) { + animationController.duration = const Duration(milliseconds: 100); + await animationController.reverse(from: 1); + _pageController.animateToPage( + index, + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + ); + setState(() { + _selectedPage = index; + }); + animationController.duration = const Duration(milliseconds: 200); + animationController.forward(from: 0); + } + }, + destinations: navigationDestinations, + )); } } diff --git a/lib/widgets/inventory/inventory_browser_app_bar.dart b/lib/widgets/inventory/inventory_browser_app_bar.dart index f2ec14e..0825b22 100644 --- a/lib/widgets/inventory/inventory_browser_app_bar.dart +++ b/lib/widgets/inventory/inventory_browser_app_bar.dart @@ -47,6 +47,8 @@ class _InventoryBrowserAppBarState extends State { ? AppBar( key: const ValueKey("default-appbar"), title: const Text("Inventory"), + centerTitle: false, + backgroundColor: Colors.transparent, actions: [ PopupMenuButton( icon: const Icon(Icons.swap_vert), @@ -148,6 +150,8 @@ class _InventoryBrowserAppBarState extends State { : AppBar( key: const ValueKey("selection-appbar"), title: Text("${iClient.selectedRecordCount} Selected"), + centerTitle: false, + backgroundColor: Colors.transparent, leading: IconButton( onPressed: () { iClient.clearSelectedRecords(); diff --git a/lib/widgets/login_screen.dart b/lib/widgets/login_screen.dart index 449340f..1b15329 100644 --- a/lib/widgets/login_screen.dart +++ b/lib/widgets/login_screen.dart @@ -6,6 +6,7 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:recon/client_holder.dart'; import 'package:recon/clients/api_client.dart'; import 'package:recon/models/authentication_data.dart'; +import 'package:recon/widgets/translucent_glass.dart'; class LoginScreen extends StatefulWidget { const LoginScreen({this.onLoginSuccessful, this.cachedUsername, super.key}); @@ -159,9 +160,31 @@ class _LoginScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text("ReCon"), - ), + appBar: PreferredSize( + preferredSize: const Size.fromHeight(kToolbarHeight), + child: TranslucentGlass( + border: Border( + bottom: TranslucentGlass.defaultBorderSide(context), + ), + gradient: TranslucentGlass.defaultTopGradient(context), + child: AppBar( + backgroundColor: Colors.transparent, + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox.square( + child: Image.asset( + "assets/images/logo.png", + width: 28, + filterQuality: FilterQuality.medium, + isAntiAlias: true, + ), + ), + const SizedBox(width: 8), + const Text("ReCon") + ], + ), + ))), body: Builder(builder: (context) { return ListView( controller: _scrollController, diff --git a/lib/widgets/marquee.dart b/lib/widgets/marquee.dart new file mode 100644 index 0000000..8a6f835 --- /dev/null +++ b/lib/widgets/marquee.dart @@ -0,0 +1,57 @@ +import 'package:flutter/widgets.dart'; + +class Marquee extends StatefulWidget { + final Widget child; + final Axis direction; + final Duration animationDuration; + + const Marquee({ + super.key, + required this.child, + this.direction = Axis.horizontal, + this.animationDuration = const Duration(milliseconds: 6000), + }); + + @override + State createState() => _MarqueeState(); +} + +class _MarqueeState extends State { + late ScrollController scrollController; + + @override + void initState() { + scrollController = ScrollController(initialScrollOffset: 50.0); + WidgetsBinding.instance.addPostFrameCallback(scroll); + super.initState(); + } + + @override + void dispose() { + scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + scrollDirection: widget.direction, + controller: scrollController, + child: widget.child, + ); + } + + void scroll(_) async { + while (scrollController.hasClients) { + if (scrollController.hasClients) { + await scrollController.animateTo( + scrollController.position.maxScrollExtent, + duration: widget.animationDuration, + curve: Curves.linear, + ); + scrollController.jumpTo(scrollController.position.minScrollExtent); + } + } + } +} diff --git a/lib/widgets/messages/message_bubble.dart b/lib/widgets/messages/message_bubble.dart index 0c2d9f0..ff79720 100644 --- a/lib/widgets/messages/message_bubble.dart +++ b/lib/widgets/messages/message_bubble.dart @@ -1,10 +1,11 @@ +import 'package:flutter/material.dart'; import 'package:recon/client_holder.dart'; +import 'package:recon/color_palette.dart'; import 'package:recon/models/message.dart'; import 'package:recon/widgets/messages/message_asset.dart'; import 'package:recon/widgets/messages/message_audio_player.dart'; import 'package:recon/widgets/messages/message_session_invite.dart'; import 'package:recon/widgets/messages/message_text.dart'; -import 'package:flutter/material.dart'; class MessageBubble extends StatelessWidget { const MessageBubble({required this.message, super.key}); @@ -14,9 +15,17 @@ class MessageBubble extends StatelessWidget { @override Widget build(BuildContext context) { final bool mine = message.senderId == ClientHolder.of(context).apiClient.userId; - final colorScheme = Theme.of(context).colorScheme; - final foregroundColor = mine ? colorScheme.onPrimaryContainer : colorScheme.onSurfaceVariant; - final backgroundColor = mine ? colorScheme.primaryContainer : colorScheme.surfaceVariant; + final MessageState state = message.state; + final ThemeData theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final foregroundColor = colorScheme.onBackground; + final backgroundColor = mine + ? switch (state) { + MessageState.local => colorScheme.secondaryContainer, + MessageState.sent => colorScheme.primaryContainer, + MessageState.read => colorScheme.primaryContainer, + } + : palette.hero.cyan.withOpacity(0.2); return Padding( padding: EdgeInsets.only(left: mine ? 32 : 12, bottom: 16, right: mine ? 12 : 32), child: Row( @@ -29,10 +38,22 @@ class MessageBubble extends StatelessWidget { child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), child: switch (message.type) { - MessageType.sessionInvite => MessageSessionInvite(message: message, foregroundColor: foregroundColor,), - MessageType.object => MessageAsset(message: message, foregroundColor: foregroundColor,), - MessageType.sound => MessageAudioPlayer(message: message, foregroundColor: foregroundColor,), - MessageType.unknown || MessageType.text => MessageText(message: message, foregroundColor: foregroundColor,) + MessageType.sessionInvite => MessageSessionInvite( + message: message, + foregroundColor: foregroundColor, + ), + MessageType.object => MessageAsset( + message: message, + foregroundColor: foregroundColor, + ), + MessageType.sound => MessageAudioPlayer( + message: message, + foregroundColor: foregroundColor, + ), + MessageType.unknown || MessageType.text => MessageText( + message: message, + foregroundColor: foregroundColor, + ) }, ), ), diff --git a/lib/widgets/messages/message_input_bar.dart b/lib/widgets/messages/message_input_bar.dart index ebe1a35..45d474d 100644 --- a/lib/widgets/messages/message_input_bar.dart +++ b/lib/widgets/messages/message_input_bar.dart @@ -15,6 +15,7 @@ import 'package:recon/clients/messaging_client.dart'; import 'package:recon/models/message.dart'; import 'package:recon/models/users/friend.dart'; import 'package:recon/widgets/messages/message_attachment_list.dart'; +import 'package:recon/widgets/translucent_glass.dart'; import 'package:record/record.dart'; class MessageInputBar extends StatefulWidget { @@ -154,7 +155,9 @@ class _MessageInputBarState extends State { @override Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); final mClient = Provider.of(context, listen: false); + return Listener( onPointerMove: _pointerMoveEventHandler, onPointerUp: (_) async { @@ -200,22 +203,18 @@ class _MessageInputBarState extends State { } } }, - child: Container( - decoration: BoxDecoration( - border: const Border(top: BorderSide(width: 1, color: Colors.black)), - color: Theme.of(context).colorScheme.surfaceVariant, - ), - padding: const EdgeInsets.symmetric(horizontal: 4), + child: TranslucentGlass.edgeToEdge( + context, + top: false, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + gradient: TranslucentGlass.defaultBottomGradient(context), child: SafeArea( - top: false, - child: Column( - children: [ - if (_isSending && _sendProgress != null) LinearProgressIndicator(value: _sendProgress), - Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, - ), - child: AnimatedSwitcher( + top: false, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (_isSending && _sendProgress != null) LinearProgressIndicator(value: _sendProgress), + AnimatedSwitcher( duration: const Duration(milliseconds: 200), switchInCurve: Curves.easeOut, switchOutCurve: Curves.easeOut, @@ -301,310 +300,317 @@ class _MessageInputBarState extends State { ), }, ), - ), - Row( - children: [ - AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - transitionBuilder: (Widget child, Animation animation) => FadeTransition( - opacity: animation, - child: RotationTransition( - turns: Tween(begin: 0.6, end: 1).animate(animation), - child: child, + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + transitionBuilder: (Widget child, Animation animation) => FadeTransition( + opacity: animation, + child: RotationTransition( + turns: Tween(begin: 0.6, end: 1).animate(animation), + child: child, + ), + ), + child: Padding( + padding: const EdgeInsets.only(bottom: 8), + child: switch ((_attachmentPickerOpen, _isRecording)) { + (_, true) => IconButton( + onPressed: () {}, + icon: Icon( + Icons.delete, + color: _recordingCancelled ? theme.colorScheme.error : null, + ), + ), + (false, _) => IconButton( + key: const ValueKey("add-attachment-icon"), + onPressed: _isSending + ? null + : () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Sorry, this feature is not yet available"))); + return; + // setState(() { + // _attachmentPickerOpen = true; + // }); + }, + icon: const Icon( + Icons.attach_file, + ), + ), + (true, _) => IconButton( + key: const ValueKey("remove-attachment-icon"), + onPressed: _isSending + ? null + : () async { + if (_loadedFiles.isNotEmpty) { + await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text("Remove all attachments"), + content: + const Text("This will remove all attachments, are you sure?"), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text("No"), + ), + TextButton( + onPressed: () { + setState(() { + _loadedFiles.clear(); + _attachmentPickerOpen = false; + }); + Navigator.of(context).pop(); + }, + child: const Text("Yes"), + ) + ], + )); + } else { + setState(() { + _attachmentPickerOpen = false; + }); + } + }, + icon: const Icon( + Icons.close, + ), + ), + }, ), ), - child: switch ((_attachmentPickerOpen, _isRecording)) { - (_, true) => IconButton( - onPressed: () {}, - icon: Icon( - Icons.delete, - color: _recordingCancelled ? Theme.of(context).colorScheme.error : null, - ), - ), - (false, _) => IconButton( - key: const ValueKey("add-attachment-icon"), - onPressed: _isSending - ? null - : () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Sorry, this feature is not yet available"))); + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 4), + child: Stack( + children: [ + TextField( + enabled: (!widget.disabled) && !_isSending, + autocorrect: true, + controller: _messageTextController, + showCursor: !_isRecording, + maxLines: 4, + minLines: 1, + onChanged: (text) { + if (text.isEmpty != _currentText.isEmpty) { + setState(() { + _currentText = text; + }); return; - // setState(() { - // _attachmentPickerOpen = true; - // }); - }, - icon: const Icon( - Icons.attach_file, - ), - ), - (true, _) => IconButton( - key: const ValueKey("remove-attachment-icon"), - onPressed: _isSending - ? null - : () async { - if (_loadedFiles.isNotEmpty) { - await showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text("Remove all attachments"), - content: const Text("This will remove all attachments, are you sure?"), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text("No"), + } + _currentText = text; + }, + style: theme.textTheme.bodyLarge, + decoration: InputDecoration( + isDense: true, + hintText: _isRecording ? "" : "Message ${widget.recipient.username}...", + hintMaxLines: 1, + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + fillColor: Colors.black26, + filled: true, + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(24), + )), + ), + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + transitionBuilder: (Widget child, Animation animation) => FadeTransition( + opacity: animation, + child: SlideTransition( + position: Tween( + begin: const Offset(0, .2), + end: const Offset(0, 0), + ).animate(animation), + child: child, + ), + ), + child: _isRecording + ? Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: _recordingCancelled + ? Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const SizedBox( + width: 8, ), - TextButton( - onPressed: () { - setState(() { - _loadedFiles.clear(); - _attachmentPickerOpen = false; - }); - Navigator.of(context).pop(); - }, - child: const Text("Yes"), - ) + const Padding( + padding: EdgeInsets.symmetric(horizontal: 8.0), + child: Icon( + Icons.cancel, + color: Colors.red, + size: 16, + ), + ), + Text("Cancel Recording", style: theme.textTheme.titleMedium), ], - )); - } else { - setState(() { - _attachmentPickerOpen = false; - }); - } - }, - icon: const Icon( - Icons.close, - ), - ), - }, - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), - child: Stack( - children: [ - TextField( - enabled: (!widget.disabled) && !_isSending, - autocorrect: true, - controller: _messageTextController, - showCursor: !_isRecording, - maxLines: 4, - minLines: 1, - onChanged: (text) { - if (text.isEmpty != _currentText.isEmpty) { - setState(() { - _currentText = text; - }); - return; - } - _currentText = text; - }, - style: Theme.of(context).textTheme.bodyLarge, - decoration: InputDecoration( - isDense: true, - hintText: _isRecording ? "" : "Message ${widget.recipient.username}...", - hintMaxLines: 1, - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - fillColor: Colors.black26, - filled: true, - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(24), - )), - ), - AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - transitionBuilder: (Widget child, Animation animation) => FadeTransition( - opacity: animation, - child: SlideTransition( - position: Tween( - begin: const Offset(0, .2), - end: const Offset(0, 0), - ).animate(animation), - child: child, - ), - ), - child: _isRecording - ? Padding( - padding: const EdgeInsets.symmetric(vertical: 12.0), - child: _recordingCancelled - ? Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const SizedBox( - width: 8, - ), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 8.0), - child: Icon( - Icons.cancel, - color: Colors.red, - size: 16, + ) + : Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const SizedBox( + width: 8, ), - ), - Text("Cancel Recording", style: Theme.of(context).textTheme.titleMedium), - ], - ) - : Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const SizedBox( - width: 8, - ), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 8.0), - child: Icon( - Icons.circle, - color: Colors.red, - size: 16, + const Padding( + padding: EdgeInsets.symmetric(horizontal: 8.0), + child: Icon( + Icons.circle, + color: Colors.red, + size: 16, + ), ), - ), - StreamBuilder( - stream: _recordingDurationStream(), - builder: (context, snapshot) { - return Text("Recording: ${snapshot.data?.format()}", - style: Theme.of(context).textTheme.titleMedium); - }), - ], - ), - ) - : const SizedBox.shrink(), - ), - ], + StreamBuilder( + stream: _recordingDurationStream(), + builder: (context, snapshot) { + return Text("Recording: ${snapshot.data?.format()}", + style: theme.textTheme.titleMedium); + }), + ], + ), + ) + : const SizedBox.shrink(), + ), + ], + ), ), ), - ), - AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - transitionBuilder: (Widget child, Animation animation) => FadeTransition( - opacity: animation, - child: RotationTransition( - turns: Tween(begin: 0.5, end: 1).animate(animation), - child: child, + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + transitionBuilder: (Widget child, Animation animation) => FadeTransition( + opacity: animation, + child: RotationTransition( + turns: Tween(begin: 0.5, end: 1).animate(animation), + child: child, + ), ), - ), - child: _currentText.isNotEmpty || _loadedFiles.isNotEmpty - ? IconButton( - key: const ValueKey("send-button"), - splashRadius: 24, - padding: EdgeInsets.zero, - onPressed: _isSending - ? null - : () async { - final cHolder = ClientHolder.of(context); - final sMsgnr = ScaffoldMessenger.of(context); - final settings = cHolder.settingsClient.currentSettings; - final toSend = List<(FileType, File)>.from(_loadedFiles); - setState(() { - _isSending = true; - _sendProgress = 0; - _attachmentPickerOpen = false; - _loadedFiles.clear(); - }); - try { - for (int i = 0; i < toSend.length; i++) { - final totalProgress = i / toSend.length; - final file = toSend[i]; - if (file.$1 == FileType.image) { - await sendImageMessage( - cHolder.apiClient, - mClient, - file.$2, - settings.machineId.valueOrDefault, - (progress) => setState(() { - _sendProgress = totalProgress + progress * 1 / toSend.length; - }), - ); - } else { - await sendRawFileMessage( - cHolder.apiClient, - mClient, - file.$2, - settings.machineId.valueOrDefault, - (progress) => setState( - () => _sendProgress = totalProgress + progress * 1 / toSend.length)); - } - } - setState(() { - _sendProgress = null; - }); + child: Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _currentText.isNotEmpty || _loadedFiles.isNotEmpty + ? IconButton( + key: const ValueKey("send-button"), + splashRadius: 24, + padding: EdgeInsets.zero, + onPressed: _isSending + ? null + : () async { + final cHolder = ClientHolder.of(context); + final sMsgnr = ScaffoldMessenger.of(context); + final settings = cHolder.settingsClient.currentSettings; + final toSend = List<(FileType, File)>.from(_loadedFiles); + setState(() { + _isSending = true; + _sendProgress = 0; + _attachmentPickerOpen = false; + _loadedFiles.clear(); + }); + try { + for (int i = 0; i < toSend.length; i++) { + final totalProgress = i / toSend.length; + final file = toSend[i]; + if (file.$1 == FileType.image) { + await sendImageMessage( + cHolder.apiClient, + mClient, + file.$2, + settings.machineId.valueOrDefault, + (progress) => setState(() { + _sendProgress = totalProgress + progress * 1 / toSend.length; + }), + ); + } else { + await sendRawFileMessage( + cHolder.apiClient, + mClient, + file.$2, + settings.machineId.valueOrDefault, + (progress) => setState(() => + _sendProgress = totalProgress + progress * 1 / toSend.length)); + } + } + setState(() { + _sendProgress = null; + }); - if (_currentText.isNotEmpty) { - await sendTextMessage(cHolder.apiClient, mClient, _messageTextController.text); - } - _messageTextController.clear(); - _currentText = ""; - _loadedFiles.clear(); - _attachmentPickerOpen = false; - } catch (e, s) { - FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s)); - sMsgnr.showSnackBar(SnackBar(content: Text("Failed to send a message: $e"))); - } - setState(() { - _isSending = false; - _sendProgress = null; - }); - widget.onMessageSent?.call(); - }, - icon: const Icon(Icons.send), - ) - : GestureDetector( - onTapUp: (_) { - _recordingCancelled = true; - }, - onTapDown: widget.disabled - ? null - : (_) async { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Sorry, this feature is not yet available"))); - return; - // HapticFeedback.vibrate(); - // final hadToAsk = - // await Permission.microphone.isDenied; - // final hasPermission = - // !await _recorder.hasPermission(); - // if (hasPermission) { - // if (context.mounted) { - // ScaffoldMessenger.of(context) - // .showSnackBar(const SnackBar( - // content: Text( - // "No permission to record audio."), - // )); - // } - // return; - // } - // if (hadToAsk) { - // // We had to ask for permissions so the user removed their finger from the record button. - // return; - // } + if (_currentText.isNotEmpty) { + await sendTextMessage( + cHolder.apiClient, mClient, _messageTextController.text); + } + _messageTextController.clear(); + _currentText = ""; + _loadedFiles.clear(); + _attachmentPickerOpen = false; + } catch (e, s) { + FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s)); + sMsgnr.showSnackBar(SnackBar(content: Text("Failed to send a message: $e"))); + } + setState(() { + _isSending = false; + _sendProgress = null; + }); + widget.onMessageSent?.call(); + }, + icon: const Icon(Icons.send), + ) + : GestureDetector( + onTapUp: (_) { + _recordingCancelled = true; + }, + onTapDown: widget.disabled + ? null + : (_) async { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Sorry, this feature is not yet available"))); + return; + // HapticFeedback.vibrate(); + // final hadToAsk = + // await Permission.microphone.isDenied; + // final hasPermission = + // !await _recorder.hasPermission(); + // if (hasPermission) { + // if (context.mounted) { + // ScaffoldMessenger.of(context) + // .showSnackBar(const SnackBar( + // content: Text( + // "No permission to record audio."), + // )); + // } + // return; + // } + // if (hadToAsk) { + // // We had to ask for permissions so the user removed their finger from the record button. + // return; + // } - // final dir = await getTemporaryDirectory(); - // await _recorder.start( - // path: "${dir.path}/A-${const Uuid().v4()}.wav", - // const RecordConfig( - // numChannels: 1, - // sampleRate: 44100, - // encoder: AudioEncoder.wav)); - // setState(() { - // _isRecording = true; - // }); - }, - child: IconButton( - icon: const Icon(Icons.mic_outlined), - onPressed: _isSending - ? null - : () { - // Empty onPressed for that sweet sweet ripple effect - }, - ), - ), - ), - ], - ), - ], - ), - ), + // final dir = await getTemporaryDirectory(); + // await _recorder.start( + // path: "${dir.path}/A-${const Uuid().v4()}.wav", + // const RecordConfig( + // numChannels: 1, + // sampleRate: 44100, + // encoder: AudioEncoder.wav)); + // setState(() { + // _isRecording = true; + // }); + }, + child: IconButton( + icon: const Icon(Icons.mic_outlined), + onPressed: _isSending + ? null + : () { + // Empty onPressed for that sweet sweet ripple effect + }, + ), + ), + ), + ), + ], + ), + ], + )), ), ); } diff --git a/lib/widgets/messages/messages_list.dart b/lib/widgets/messages/messages_list.dart index 2c2539e..50568d1 100644 --- a/lib/widgets/messages/messages_list.dart +++ b/lib/widgets/messages/messages_list.dart @@ -7,6 +7,7 @@ import 'package:recon/widgets/default_error_widget.dart'; import 'package:recon/widgets/friends/friend_online_status_indicator.dart'; import 'package:recon/widgets/messages/message_input_bar.dart'; import 'package:recon/widgets/messages/messages_session_header.dart'; +import 'package:recon/widgets/translucent_glass.dart'; import 'message_bubble.dart'; @@ -52,52 +53,62 @@ class _MessagesListState extends State with SingleTickerProviderSt @override Widget build(BuildContext context) { - final appBarColor = Theme.of(context).colorScheme.surface; + final ThemeData theme = Theme.of(context); + final appBarColor = theme.colorScheme.surface; + return Consumer(builder: (context, mClient, _) { final friend = mClient.selectedFriend ?? Friend.empty(); final cache = mClient.getUserMessageCache(friend.id); final sessions = friend.userStatus.decodedSessions.where((element) => element.isVisible).toList(); + return Scaffold( - appBar: AppBar( - title: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - FriendOnlineStatusIndicator(friend: friend), - const SizedBox( - width: 8, - ), - Text(friend.username), - if (friend.isHeadless) - Padding( - padding: const EdgeInsets.only(left: 12), - child: Icon( - Icons.dns, - size: 18, - color: Theme.of(context).colorScheme.onSecondaryContainer.withAlpha(150), + backgroundColor: theme.colorScheme.background, + extendBodyBehindAppBar: true, + extendBody: true, + appBar: PreferredSize( + preferredSize: const Size.fromHeight(kToolbarHeight - 8), + child: TranslucentGlass.edgeToEdge(context, + gradient: TranslucentGlass.defaultTopGradient(context), + child: AppBar( + title: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + FriendOnlineStatusIndicator(friend), + const SizedBox( + width: 8, + ), + Text(friend.username), + if (friend.isHeadless) + Padding( + padding: const EdgeInsets.only(left: 12), + child: Icon( + Icons.dns, + size: 18, + color: Theme.of(context).colorScheme.onSecondaryContainer.withAlpha(150), + ), + ), + ], ), - ), - ], - ), - actions: [ - if (sessions.isNotEmpty) - AnimatedRotation( - turns: _sessionListOpen ? -1 / 4 : 1 / 4, - duration: const Duration(milliseconds: 200), - child: IconButton( - onPressed: () { - setState(() { - _sessionListOpen = !_sessionListOpen; - }); - }, - icon: const Icon(Icons.chevron_right), - ), - ), - const SizedBox( - width: 4, - ) - ], - scrolledUnderElevation: 0.0, - ), + actions: [ + if (sessions.isNotEmpty) + AnimatedRotation( + turns: _sessionListOpen ? -1 / 4 : 1 / 4, + duration: const Duration(milliseconds: 200), + child: IconButton( + onPressed: () { + setState(() { + _sessionListOpen = !_sessionListOpen; + }); + }, + icon: const Icon(Icons.chevron_right), + ), + ), + const SizedBox( + width: 4, + ) + ], + scrolledUnderElevation: 0.0, + ))), body: Column( children: [ if (sessions.isNotEmpty) @@ -220,15 +231,15 @@ class _MessagesListState extends State with SingleTickerProviderSt ], ), ), - MessageInputBar( - recipient: friend, - disabled: cache == null || cache.error != null, - onMessageSent: () { - setState(() {}); - }, - ), ], ), + bottomNavigationBar: MessageInputBar( + recipient: friend, + disabled: cache == null || cache.error != null, + onMessageSent: () { + setState(() {}); + }, + ), ); }); } diff --git a/lib/widgets/my_profile_dialog.dart b/lib/widgets/my_profile_dialog.dart index 7e5b49b..d624776 100644 --- a/lib/widgets/my_profile_dialog.dart +++ b/lib/widgets/my_profile_dialog.dart @@ -1,9 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; import 'package:recon/apis/user_api.dart'; import 'package:recon/auxiliary.dart'; import 'package:recon/client_holder.dart'; +import 'package:recon/color_palette.dart'; import 'package:recon/models/personal_profile.dart'; +import 'package:recon/widgets/blend_mask.dart'; import 'package:recon/widgets/default_error_widget.dart'; import 'package:recon/widgets/generic_avatar.dart'; @@ -33,73 +36,167 @@ class _MyProfileDialogState extends State { @override Widget build(BuildContext context) { - final tt = Theme.of(context).textTheme; + final ThemeData theme = Theme.of(context); + final textTheme = theme.textTheme; + final colorScheme = theme.colorScheme; DateFormat dateFormat = DateFormat.yMd(); + return Dialog( + clipBehavior: Clip.antiAlias, + backgroundColor: colorScheme.background, child: FutureBuilder( future: _personalProfileFuture, builder: (context, snapshot) { if (snapshot.hasData) { final profile = snapshot.data as PersonalProfile; return Padding( - padding: const EdgeInsets.all(24), + padding: const EdgeInsets.only(bottom: 16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(profile.username, style: tt.titleLarge), - Text( - profile.email, - style: - tt.labelMedium?.copyWith(color: Theme.of(context).colorScheme.onSurface.withAlpha(150)), - ) - ], - ), - GenericAvatar( - imageUri: Aux.resdbToHttp(profile.userProfile.iconUrl), - radius: 24, - ) - ], + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: colorScheme.background.withOpacity(0.4), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + GenericAvatar( + imageUri: Aux.resdbToHttp(profile.userProfile.iconUrl), + radius: 32, + ), + const SizedBox( + width: 12, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(profile.username, style: textTheme.titleLarge), + Text( + profile.accountType.label, + style: textTheme.labelMedium?.copyWith(color: profile.accountType.color), + ), + ], + ), + ], + ), ), const SizedBox( - height: 16, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "User ID: ", - style: tt.labelLarge, - ), - Text(profile.id) - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "2FA: ", - style: tt.labelLarge, - ), - Text(profile.twoFactor ? "Enabled" : "Disabled") - ], + height: 12, ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Patreon Supporter: ", - style: tt.labelLarge, - ), - Text(profile.isPatreonSupporter ? "Yes" : "No") - ], + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Account Info", + style: textTheme.titleMedium, + ), + const SizedBox( + height: 8, + ), + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + color: colorScheme.background.withOpacity(0.4), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "User ID", + style: textTheme.titleSmall, + ), + Text(profile.id, + style: textTheme.bodySmall?.copyWith(color: const Color(0xFFE1E1E0))) + ], + ), + IconButton( + onPressed: () { + Clipboard.setData(ClipboardData(text: profile.id)); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text("User ID copied to clipboard"), + behavior: SnackBarBehavior.floating)); + }, + icon: const Icon(Icons.copy_outlined)) + ], + ), + const SizedBox( + height: 8, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Email", + style: textTheme.titleSmall, + ), + Text(profile.email, + style: textTheme.bodySmall?.copyWith(color: const Color(0xFFE1E1E0))) + ], + ), + IconButton( + onPressed: () { + Clipboard.setData(ClipboardData(text: profile.email)); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text("Email copied to clipboard"), + behavior: SnackBarBehavior.floating)); + }, + icon: const Icon(Icons.copy_outlined)) + ], + ), + const SizedBox( + height: 8, + ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Patreon Supporter", + style: textTheme.titleSmall, + ), + Text(profile.isPatreonSupporter ? "Yes" : "No", + style: textTheme.bodySmall?.copyWith(color: const Color(0xFFE1E1E0))) + ], + ), + const SizedBox( + height: 8, + ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Two Factor Authentication", + style: textTheme.titleSmall, + ), + Text(profile.twoFactor ? "Enabled" : "Disabled", + style: textTheme.bodySmall?.copyWith(color: const Color(0xFFE1E1E0))) + ], + ), + ])), + ), + ], + ), ), if (profile.publicBanExpiration?.isAfter(DateTime.now()) ?? false) Row( @@ -107,11 +204,14 @@ class _MyProfileDialogState extends State { children: [ Text( "Ban Expiration: ", - style: tt.labelLarge, + style: textTheme.labelLarge, ), Text(dateFormat.format(profile.publicBanExpiration!)) ], ), + const SizedBox( + height: 12, + ), FutureBuilder( future: _storageQuotaFuture, builder: (context, snapshot) { @@ -121,9 +221,6 @@ class _MyProfileDialogState extends State { maxBytes: storage?.fullQuotaBytes ?? 1, ); }), - const SizedBox( - height: 12, - ), ], ), ); @@ -166,30 +263,61 @@ class StorageIndicator extends StatelessWidget { @override Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); final value = usedBytes / maxBytes; + return Padding( - padding: const EdgeInsets.only(top: 16.0), + padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("Storage:", style: Theme.of(context).textTheme.titleMedium), - Text(// Displayed in GiB instead of GB for consistency with Resonite - "${(usedBytes * 9.3132257461548e-10).toStringAsFixed(2)}/${(maxBytes * 9.3132257461548e-10).toStringAsFixed(2)} GB"), - ], - ), + Text("Storage", style: theme.textTheme.titleMedium), const SizedBox( height: 8, ), ClipRRect( - borderRadius: BorderRadius.circular(8), - child: LinearProgressIndicator( - minHeight: 12, - color: value > 0.95 ? Theme.of(context).colorScheme.error : null, - value: value, - ), + borderRadius: BorderRadius.circular(12), + child: Stack(children: [ + LinearProgressIndicator( + value: value, + minHeight: 48, + color: value >= 0.95 ? palette.hero.red.withOpacity(0.7) : palette.hero.cyan, + backgroundColor: value >= 0.95 ? palette.hero.red.withOpacity(0.3) : palette.sub.cyan, + ), + Container( + height: 48, + padding: const EdgeInsets.symmetric(horizontal: 12), + alignment: Alignment.center, + child: BlendMask( + blendMode: BlendMode.srcATop, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "${(value * 100).toStringAsFixed(0)}%", + style: theme.textTheme.titleMedium + ?.copyWith(color: palette.neutrals.light, fontWeight: FontWeight.bold), + ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + // Displayed in GiB instead of GB for consistency with Resonite + Text( + "${(usedBytes * 9.3132257461548e-10).toStringAsFixed(2)} GB of ${(maxBytes * 9.3132257461548e-10).toStringAsFixed(2)} GB", + style: theme.textTheme.bodyMedium?.copyWith(color: palette.neutrals.light), + ), + Text( + "Storage Space Used", + style: + theme.textTheme.labelSmall?.copyWith(color: palette.neutrals.light, fontSize: 10), + ), + ]), + ], + ), + )), + ]), ) ], ), diff --git a/lib/widgets/panorama.dart b/lib/widgets/panorama.dart index 800dfd5..bb5c06e 100644 --- a/lib/widgets/panorama.dart +++ b/lib/widgets/panorama.dart @@ -204,16 +204,16 @@ // Adapted from https://github.com/zesage/panorama to remove nonfunctional motion sensor control and fix any linting // warnings - import 'dart:async'; -import 'dart:ui' as ui; import 'dart:math' as math; +import 'dart:ui' as ui; + import 'package:flutter/material.dart'; import 'package:flutter_cube/flutter_cube.dart'; class Panorama extends StatefulWidget { const Panorama({ - Key? key, + super.key, this.latitude = 0, this.longitude = 0, this.zoom = 1.0, @@ -240,7 +240,7 @@ class Panorama extends StatefulWidget { this.onImageLoad, this.child, this.hotspots, - }) : super(key: key); + }); /// The initial latitude, in degrees, between -90 and 90. default to 0 (the vertical center of the image). final double latitude; @@ -567,7 +567,6 @@ class _PanoramaState extends State with SingleTickerProviderStateMixin _streamController = StreamController.broadcast(); _stream = _streamController.stream; - _controller = AnimationController(duration: const Duration(milliseconds: 60000), vsync: this) ..addListener(_updateView); if (widget.animSpeed != 0) _controller.repeat(); diff --git a/lib/widgets/recon_navbar.dart b/lib/widgets/recon_navbar.dart new file mode 100644 index 0000000..15149a1 --- /dev/null +++ b/lib/widgets/recon_navbar.dart @@ -0,0 +1,189 @@ +import 'package:flutter/material.dart'; +import 'package:recon/color_palette.dart'; +import 'package:recon/widgets/blend_mask.dart'; +import 'package:recon/widgets/translucent_glass.dart'; + +class _ReConNavigationContext extends InheritedWidget { + const _ReConNavigationContext({ + required this.index, + required this.selectedIndex, + required this.animationController, + required this.onTap, + required super.child, + }); + + final int index; + final int selectedIndex; + final AnimationController animationController; + final VoidCallback onTap; + + static _ReConNavigationContext of(BuildContext context) { + final _ReConNavigationContext? result = context.dependOnInheritedWidgetOfExactType<_ReConNavigationContext>(); + assert( + result != null, + 'ReCon navigation destinations need a ReConNavigationContext parent, ' + 'which must be accounted for by you.', + ); + result?.initContext(); + return result!; + } + + void initContext() { + if (selectedIndex == index) { + animationController.forward(from: 0); + } + } + + Animation get selectedAnimation { + return index == selectedIndex + ? Tween(begin: 0, end: 1).animate( + CurvedAnimation( + parent: animationController, + curve: Curves.easeOut, + ), + ) + : kAlwaysDismissedAnimation; + } + + @override + bool updateShouldNotify(_ReConNavigationContext oldWidget) { + return selectedIndex != oldWidget.selectedIndex || animationController != oldWidget.animationController; + } +} + +class ReConNavigationDestination extends StatelessWidget { + final String label; + final Widget icon; + final String color; + + const ReConNavigationDestination({ + super.key, + required this.label, + required this.icon, + required this.color, + }); + + @override + Widget build(BuildContext context) { + final _ReConNavigationContext navContext = _ReConNavigationContext.of(context); + final ThemeData theme = Theme.of(context); + + final Animation animation = navContext.selectedAnimation; + final DecorationTween iconBoxDecoration = DecorationTween( + begin: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + theme.colorScheme.onSurface.withOpacity(0.1), + ], + ), + ), + end: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + palette.hero[color].withOpacity(0), + palette.hero[color].withOpacity(0.8), + ], + ), + ), + ); + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 4), + child: SizedBox( + height: 48, + child: InkWell( + onTap: navContext.onTap, + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + BlendMask( + blendMode: BlendMode.srcOver, + child: SizedBox( + width: 58, + height: 31, + child: DecoratedBoxTransition( + decoration: iconBoxDecoration.animate(animation), + child: Center( + child: IconTheme( + data: IconThemeData( + color: palette.hero[color], + size: 24, + ), + child: icon, + ), + ), + ), + ), + ), + Container( + width: 68, + decoration: BoxDecoration( + color: palette.mid[color], + borderRadius: const BorderRadius.all(Radius.circular(6)), + ), + child: AnimatedDefaultTextStyle( + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + duration: const Duration(milliseconds: 100), + child: Text(label), + ), + ), + ], + ), + ), + ), + ); + } +} + +class ReConNavigationBar extends StatelessWidget { + const ReConNavigationBar({ + super.key, + required this.animationController, + required this.selectedIndex, + required this.destinations, + this.onDestinationSelected, + }); + + final AnimationController animationController; + final int selectedIndex; + final List destinations; + final ValueChanged? onDestinationSelected; + + @override + Widget build(BuildContext context) { + return TranslucentGlass.bottomNavBar( + context, + gradient: TranslucentGlass.defaultBottomGradient(context), + child: SafeArea( + top: false, + minimum: const EdgeInsets.only(top: 8, bottom: 16, left: 8, right: 8), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: destinations + .map( + (e) => _ReConNavigationContext( + index: destinations.indexOf(e), + selectedIndex: selectedIndex, + animationController: animationController, + onTap: () { + if (onDestinationSelected != null) onDestinationSelected!(destinations.indexOf(e)); + }, + child: e), + ) + .toList(), + ), + ), + ); + } +} diff --git a/lib/widgets/sessions/session_list.dart b/lib/widgets/sessions/session_list.dart index 3565432..431e05f 100644 --- a/lib/widgets/sessions/session_list.dart +++ b/lib/widgets/sessions/session_list.dart @@ -1,12 +1,13 @@ import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:recon/auxiliary.dart'; import 'package:recon/clients/session_client.dart'; import 'package:recon/models/session.dart'; import 'package:recon/widgets/default_error_widget.dart'; import 'package:recon/widgets/formatted_text.dart'; import 'package:recon/widgets/sessions/session_view.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; +import 'package:recon/widgets/translucent_glass.dart'; class SessionList extends StatefulWidget { const SessionList({super.key}); @@ -27,6 +28,8 @@ class _SessionListState extends State with AutomaticKeepAliveClient @override Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + super.build(context); return ChangeNotifierProvider.value( value: Provider.of(context), @@ -36,73 +39,66 @@ class _SessionListState extends State with AutomaticKeepAliveClient future: sClient.sessionsFuture, builder: (context, snapshot) { final data = snapshot.data ?? []; - return Stack( - children: [ - RefreshIndicator( - onRefresh: () async { - sClient.reloadSessions(); - try { - await sClient.sessionsFuture; - } catch (e) { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); - } - } - }, - child: data.isEmpty && snapshot.connectionState == ConnectionState.done + return RefreshIndicator( + onRefresh: () async { + sClient.reloadSessions(); + try { + await sClient.sessionsFuture; + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); + } + } + }, + child: Stack( + children: [ + data.isEmpty && snapshot.connectionState == ConnectionState.done ? const DefaultErrorWidget( title: "No Sessions Found", message: "Try to adjust your filters", iconOverride: Icons.public_off, ) : Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), + padding: const EdgeInsets.symmetric(horizontal: 8), child: GridView.builder( padding: const EdgeInsets.only(top: 10), itemCount: data.length, gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 256, - crossAxisSpacing: 4, - mainAxisSpacing: 4, + crossAxisSpacing: 12, + mainAxisSpacing: 12, childAspectRatio: .8, ), itemBuilder: (context, index) { final session = data[index]; - return Card( - elevation: 0, - shape: RoundedRectangleBorder( - side: BorderSide( - color: Theme.of(context).colorScheme.outline, - ), - borderRadius: BorderRadius.circular(16), - ), + return TranslucentGlass.card( + context, + padding: const EdgeInsets.all(0), + borderRadius: BorderRadius.circular(18), + gradient: TranslucentGlass.defaultTopGradient(context), child: InkWell( onTap: () { Navigator.of(context) .push(MaterialPageRoute(builder: (context) => SessionView(session: session))); }, - borderRadius: BorderRadius.circular(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( flex: 5, - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: Hero( - tag: session.id, - child: CachedNetworkImage( - imageUrl: Aux.resdbToHttp(session.thumbnailUrl), - fit: BoxFit.cover, - errorWidget: (context, url, error) => const Center( - child: Icon( - Icons.broken_image, - size: 64, - ), + child: Hero( + tag: session.id, + child: CachedNetworkImage( + imageUrl: Aux.resdbToHttp(session.thumbnailUrl), + fit: BoxFit.cover, + errorWidget: (context, url, error) => const Center( + child: Icon( + Icons.broken_image, + size: 64, ), - placeholder: (context, uri) => - const Center(child: CircularProgressIndicator()), ), + placeholder: (context, uri) => + const Center(child: CircularProgressIndicator()), ), ), ), @@ -135,12 +131,9 @@ class _SessionListState extends State with AutomaticKeepAliveClient "${session.sessionUsers.length.toString().padLeft(2, "0")}/${session.maxUsers.toString().padLeft(2, "0")} Online", maxLines: 1, overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context) - .colorScheme - .onSurface - .withOpacity(.5), - ), + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurface.withOpacity(.5), + ), ), ), ], @@ -156,9 +149,9 @@ class _SessionListState extends State with AutomaticKeepAliveClient }, ), ), - ), - if (snapshot.connectionState == ConnectionState.waiting) const LinearProgressIndicator() - ], + if (snapshot.connectionState == ConnectionState.waiting) const LinearProgressIndicator() + ], + ), ); }, ); diff --git a/lib/widgets/sessions/session_list_app_bar.dart b/lib/widgets/sessions/session_list_app_bar.dart index 205eee7..53a369a 100644 --- a/lib/widgets/sessions/session_list_app_bar.dart +++ b/lib/widgets/sessions/session_list_app_bar.dart @@ -15,6 +15,8 @@ class _SessionListAppBarState extends State { Widget build(BuildContext context) { return AppBar( title: const Text("Sessions"), + centerTitle: false, + backgroundColor: Colors.transparent, actions: [ Padding( padding: const EdgeInsets.only(right: 4.0), diff --git a/lib/widgets/settings_app_bar.dart b/lib/widgets/settings_app_bar.dart index 4a2ed02..5644e18 100644 --- a/lib/widgets/settings_app_bar.dart +++ b/lib/widgets/settings_app_bar.dart @@ -7,6 +7,8 @@ class SettingsAppBar extends StatelessWidget { Widget build(BuildContext context) { return AppBar( title: const Text("Settings"), + centerTitle: false, + backgroundColor: Colors.transparent, ); } } diff --git a/lib/widgets/translucent_glass.dart b/lib/widgets/translucent_glass.dart new file mode 100644 index 0000000..1c70431 --- /dev/null +++ b/lib/widgets/translucent_glass.dart @@ -0,0 +1,240 @@ +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:recon/color_palette.dart'; +import 'package:recon/device_info.dart'; +import 'package:recon/widgets/marquee.dart'; +import 'package:smooth_corner/smooth_corner.dart'; + +class TranslucentGlass extends StatelessWidget { + const TranslucentGlass({ + super.key, + this.margin = EdgeInsets.zero, + this.padding = EdgeInsets.zero, + this.shape, + this.border = const Border.fromBorderSide(BorderSide(color: Colors.white54, width: 1)), + this.borderRadius, + this.color, + this.gradient, + required this.child, + }); + + final EdgeInsetsGeometry margin; + final EdgeInsetsGeometry padding; + final ShapeBorder? shape; + final Border border; + final BorderRadiusGeometry? borderRadius; + final Color? color; + final Gradient? gradient; + final Widget child; + + factory TranslucentGlass.edgeToEdge( + BuildContext context, { + required Widget child, + EdgeInsetsGeometry padding = EdgeInsets.zero, + Color? color, + Gradient? gradient, + bool top = true, + }) { + final BorderSide borderSide = defaultBorderSide(context); + + return TranslucentGlass( + padding: padding, + border: Border( + top: !top ? borderSide : BorderSide.none, + bottom: top ? borderSide : BorderSide.none, + ), + color: color, + gradient: gradient, + child: child, + ); + } + + factory TranslucentGlass.island( + BuildContext context, { + required Widget child, + EdgeInsetsGeometry padding = EdgeInsets.zero, + ShapeBorder? shape, + BorderRadiusGeometry? borderRadius, + Color? color, + Gradient? gradient, + }) { + final radius = borderRadius ?? BorderRadius.circular(24); + final side = defaultBorderSide(context); + + return TranslucentGlass( + padding: padding, + shape: shape ?? RoundedRectangleBorder(side: side, borderRadius: radius), + border: Border.fromBorderSide(side), + borderRadius: radius, + color: color, + gradient: gradient, + child: child, + ); + } + + static Widget bottomNavBar( + BuildContext context, { + required Widget child, + EdgeInsetsGeometry padding = EdgeInsets.zero, + Color? color, + Gradient? gradient, + }) { + final BorderSide side = defaultBorderSide(context); + final Radius? bezelRadius = DeviceInfo.bezelRadius; + final bool floating = bezelRadius != null; + final double margin = floating ? 12 : 0; + final Radius calculatedRadius = floating ? bezelRadius - Radius.circular(margin - 1) : Radius.zero; + final BorderRadius? borderRadius = floating + ? BorderRadius.only( + topLeft: const Radius.circular(24), + topRight: const Radius.circular(24), + bottomLeft: calculatedRadius, + bottomRight: calculatedRadius, + ) + : null; + + return TranslucentGlass( + margin: EdgeInsets.only(left: margin, right: margin, bottom: margin), + padding: padding, + borderRadius: borderRadius, + border: floating ? Border.fromBorderSide(side) : Border(top: side), + color: color, + gradient: gradient, + child: child, + ); + } + + factory TranslucentGlass.card(BuildContext context, + {required Widget child, + EdgeInsetsGeometry padding = EdgeInsets.zero, + ShapeBorder? shape, + BorderRadiusGeometry? borderRadius, + Color? color, + Gradient? gradient}) { + final radius = borderRadius ?? BorderRadius.circular(12); + final side = defaultBorderSide(context); + + return TranslucentGlass( + padding: padding, + shape: shape ?? RoundedRectangleBorder(side: side, borderRadius: radius), + borderRadius: radius, + border: Border.fromBorderSide(side), + color: color, + gradient: gradient, + child: child, + ); + } + + @override + Widget build(BuildContext context) { + final ShapeBorder shapeBorder = shape ?? + SmoothRectangleBorder( + smoothness: 0.6, + borderRadius: borderRadius ?? BorderRadius.zero, + side: border.isUniform ? border.top : BorderSide.none, + ); + + return GestureDetector( + behavior: HitTestBehavior.opaque, + child: Container( + margin: margin, + child: ClipPath( + clipper: ShapeBorderClipper( + shape: shapeBorder, + ), + clipBehavior: Clip.antiAlias, + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 10.0, + sigmaY: 10.0, + ), + child: Stack( + children: [ + if (kDebugMode) // Debugging + UnconstrainedBox( + constrainedAxis: Axis.horizontal, + alignment: AlignmentDirectional.center, + child: Marquee( + direction: Axis.horizontal, + animationDuration: const Duration(milliseconds: 500 * 18), + child: Row( + mainAxisSize: MainAxisSize.max, + children: List.filled( + 18, + Container( + margin: const EdgeInsets.symmetric(horizontal: 8), + child: Text( + 'owo', + textAlign: TextAlign.center, + style: TextStyle( + color: palette.neutrals.light.withOpacity(0.1), + fontSize: 32, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ), + ), + Container( + padding: padding.add(border.dimensions), + decoration: border.isUniform + ? ShapeDecoration( + color: color, + gradient: gradient, + shape: shapeBorder, + ) + : BoxDecoration( + borderRadius: borderRadius, + border: border, + color: color, + gradient: gradient, + ), + child: child, + ), + ], + ), + ), + ), + ), + ); + } + + static LinearGradient defaultTopGradient(BuildContext context) { + final theme = Theme.of(context); + + return LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + theme.colorScheme.background, + theme.colorScheme.surface, + ], + ); + } + + static LinearGradient defaultBottomGradient(BuildContext context) { + final theme = Theme.of(context); + + return LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + theme.colorScheme.surface, + theme.colorScheme.background, + ], + ); + } + + static BorderSide defaultBorderSide(BuildContext context) { + final theme = Theme.of(context); + + return BorderSide( + color: theme.colorScheme.outline, + width: 1, + ); + } +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 193930b..dcad730 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,7 @@ import FlutterMacOS import Foundation import audio_session +import device_info_plus import dynamic_color import ffmpeg_kit_flutter_audio import file_selector_macos @@ -21,6 +22,7 @@ import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) + DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) FFmpegKitFlutterPlugin.register(with: registry.registrar(forPlugin: "FFmpegKitFlutterPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 33dfd00..88951bb 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -109,4 +109,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 3efd3b4b57928fa6a5be6b71a1f5dc6e2a2b54af -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 diff --git a/pubspec.lock b/pubspec.lock index d788584..16a0db3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + apple_product_name: + dependency: "direct main" + description: + name: apple_product_name + sha256: "03058df2abf4880f92f9c3d4166c0e4165bdfcb7e0718e6940ec44f460e60a74" + url: "https://pub.dev" + source: hosted + version: "2.4.0" args: dependency: transitive description: @@ -169,6 +177,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.8" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110" + url: "https://pub.dev" + source: hosted + version: "9.1.2" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + url: "https://pub.dev" + source: hosted + version: "7.0.0" dynamic_color: dependency: "direct main" description: @@ -837,6 +861,14 @@ packages: description: flutter source: sdk version: "0.0.99" + smooth_corner: + dependency: "direct main" + description: + name: smooth_corner + sha256: "1e920cffd9644d6f51f9a99674652f8c00f2e9074b275f3edde0de1441ba78e9" + url: "https://pub.dev" + source: hosted + version: "1.1.0" source_span: dependency: transitive description: @@ -1045,6 +1077,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.9" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" workmanager: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 7fc7ea6..6b4b6a9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,6 +65,9 @@ dependencies: share_plus: ^7.1.0 ffmpeg_kit_flutter_audio: ^6.0.3 background_downloader: ^7.12.2 + device_info_plus: ^9.1.2 + apple_product_name: ^2.4.0 + smooth_corner: ^1.1.0 dev_dependencies: flutter_test: