feat: import Chinese-localized Buzz source snapshot
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
@@ -0,0 +1,36 @@
|
||||
**/dgph
|
||||
*.mode1v3
|
||||
*.mode2v3
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
*.perspectivev3
|
||||
**/*sync/
|
||||
.sconsign.dblite
|
||||
.tags*
|
||||
**/.vagrant/
|
||||
**/DerivedData/
|
||||
Icon?
|
||||
**/Pods/
|
||||
**/.symlinks/
|
||||
profile
|
||||
xcuserdata
|
||||
**/.generated/
|
||||
Flutter/App.framework
|
||||
Flutter/AppOverrides.xcconfig
|
||||
Flutter/WorktreeOverrides.xcconfig
|
||||
Flutter/Flutter.framework
|
||||
Flutter/Flutter.podspec
|
||||
Flutter/Generated.xcconfig
|
||||
Flutter/ephemeral/
|
||||
Flutter/app.flx
|
||||
Flutter/app.zip
|
||||
Flutter/flutter_assets/
|
||||
Flutter/flutter_export_environment.sh
|
||||
ServiceDefinitions.json
|
||||
Runner/GeneratedPluginRegistrant.*
|
||||
|
||||
# Exceptions to above rules.
|
||||
!default.mode1v3
|
||||
!default.mode2v3
|
||||
!default.pbxuser
|
||||
!default.perspectivev3
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>App</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>io.flutter.flutter.app</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>App</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,21 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
|
||||
// Default app bundle identifier. Internal/custom builds can override this
|
||||
// without patching tracked files by writing
|
||||
// `mobile/ios/Flutter/AppOverrides.xcconfig` containing
|
||||
// `BUNDLE_IDENTIFIER = your.app.id` (gitignored).
|
||||
BUNDLE_IDENTIFIER = com.buzz.buzzMobile
|
||||
APP_DISPLAY_NAME = Buzz
|
||||
|
||||
// Worktree-aware debug identity (gitignored, written by
|
||||
// scripts/mobile-worktree-overrides.sh): a per-worktree bundle identifier
|
||||
// and a branch-labelled display name so debug builds from multiple worktrees
|
||||
// install side by side. Release and Profile builds never include it.
|
||||
#include? "WorktreeOverrides.xcconfig"
|
||||
|
||||
// Developer app-specific overrides are included last: xcconfig
|
||||
// later-include-wins is per variable, so a personal BUNDLE_IDENTIFIER for
|
||||
// device signing beats the worktree default while unset variables still
|
||||
// fall through to the worktree values.
|
||||
#include? "AppOverrides.xcconfig"
|
||||
@@ -0,0 +1,11 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
|
||||
// Defaults for the iOS Release build. Internal/custom builds (e.g. an
|
||||
// enterprise-signed distribution) override these without patching tracked
|
||||
// files by writing `mobile/ios/Flutter/AppOverrides.xcconfig` (gitignored).
|
||||
BUNDLE_IDENTIFIER = com.buzz.buzzMobile
|
||||
APP_DISPLAY_NAME = Buzz
|
||||
CODE_SIGN_STYLE = Automatic
|
||||
CODE_SIGN_IDENTITY = iPhone Developer
|
||||
#include? "AppOverrides.xcconfig"
|
||||
@@ -0,0 +1,42 @@
|
||||
platform :ios, '16.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
project 'Runner', {
|
||||
'Debug' => :debug,
|
||||
'Profile' => :release,
|
||||
'Release' => :release,
|
||||
}
|
||||
|
||||
def flutter_root
|
||||
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
|
||||
unless File.exist?(generated_xcode_build_settings_path)
|
||||
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
|
||||
end
|
||||
|
||||
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||
return matches[1].strip if matches
|
||||
end
|
||||
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
|
||||
end
|
||||
|
||||
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||
|
||||
flutter_ios_podfile_setup
|
||||
|
||||
target 'Runner' do
|
||||
use_frameworks!
|
||||
|
||||
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||
target 'RunnerTests' do
|
||||
inherit! :search_paths
|
||||
end
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,111 @@
|
||||
PODS:
|
||||
- app_badge_plus (1.2.10):
|
||||
- Flutter
|
||||
- app_links (6.4.1):
|
||||
- Flutter
|
||||
- camera_avfoundation (0.0.1):
|
||||
- Flutter
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- file_selector_ios (0.0.1):
|
||||
- Flutter
|
||||
- Flutter (1.0.0)
|
||||
- flutter_secure_storage_darwin (10.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- image_picker_ios (0.0.1):
|
||||
- Flutter
|
||||
- mobile_scanner (7.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- open_filex (0.0.2):
|
||||
- Flutter
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- photo_manager (3.11.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- video_player_avfoundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- app_badge_plus (from `.symlinks/plugins/app_badge_plus/ios`)
|
||||
- app_links (from `.symlinks/plugins/app_links/ios`)
|
||||
- camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- file_selector_ios (from `.symlinks/plugins/file_selector_ios/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`)
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
|
||||
- open_filex (from `.symlinks/plugins/open_filex/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- photo_manager (from `.symlinks/plugins/photo_manager/darwin`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
app_badge_plus:
|
||||
:path: ".symlinks/plugins/app_badge_plus/ios"
|
||||
app_links:
|
||||
:path: ".symlinks/plugins/app_links/ios"
|
||||
camera_avfoundation:
|
||||
:path: ".symlinks/plugins/camera_avfoundation/ios"
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
file_selector_ios:
|
||||
:path: ".symlinks/plugins/file_selector_ios/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_secure_storage_darwin:
|
||||
:path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin"
|
||||
image_picker_ios:
|
||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||
mobile_scanner:
|
||||
:path: ".symlinks/plugins/mobile_scanner/darwin"
|
||||
open_filex:
|
||||
:path: ".symlinks/plugins/open_filex/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
photo_manager:
|
||||
:path: ".symlinks/plugins/photo_manager/darwin"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
video_player_avfoundation:
|
||||
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_badge_plus: 09939f19a075cc742cc155d8ed85e6d8601f0104
|
||||
app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a
|
||||
camera_avfoundation: 968a9a5323c79a99c166ad9d7866bfd2047b5a9b
|
||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||
file_selector_ios: ec57ec07954363dd730b642e765e58f199bb621a
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23
|
||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
|
||||
open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
photo_manager: 6ab48c2ce7ec21aa06d59e6cc049f0b6d9ba7f94
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a
|
||||
|
||||
PODFILE CHECKSUM: bd29822c3d5baf6b44b726f00ea3293a19339ef2
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
@@ -0,0 +1,776 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
331C809B294A63AB00263BE5 /* MediaSanitizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C809A294A618700263BE5 /* MediaSanitizer.swift */; };
|
||||
4A71C0012F40100100A17E01 /* InlinePhotoPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A71C0022F40100100A17E01 /* InlinePhotoPicker.swift */; };
|
||||
4A71C0032F40200100A17E01 /* NativeAttachmentPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A71C0042F40200100A17E01 /* NativeAttachmentPopover.swift */; };
|
||||
4A71C0052F40300100A17E01 /* NativeAttachmentPopoverCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A71C0062F40300100A17E01 /* NativeAttachmentPopoverCoordinator.swift */; };
|
||||
4A71C0072F40400100A17E01 /* ConcentricSheetSurface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A71C0082F40400100A17E01 /* ConcentricSheetSurface.swift */; };
|
||||
331C809D294A63AB00263BE5 /* UIKitEncoded.png in Resources */ = {isa = PBXBuildFile; fileRef = 331C809C294A618700263BE5 /* UIKitEncoded.png */; };
|
||||
331C809F294A63AB00263BE5 /* UIKitEncoded.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 331C809E294A618700263BE5 /* UIKitEncoded.jpg */; };
|
||||
33ADD70AB275E0EC81295559 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8906419FB4E98B4B12B7A56F /* Pods_Runner.framework */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
42C129326CE4E1B8E617B9CD /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD6B899582D0416ADBD8A68F /* Pods_RunnerTests.framework */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.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 */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||
remoteInfo = Runner;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
30CE81D3D1E0B195EF2A6390 /* 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 = "<group>"; };
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
331C809A294A618700263BE5 /* MediaSanitizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaSanitizer.swift; sourceTree = "<group>"; };
|
||||
4A71C0022F40100100A17E01 /* InlinePhotoPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlinePhotoPicker.swift; sourceTree = "<group>"; };
|
||||
4A71C0042F40200100A17E01 /* NativeAttachmentPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeAttachmentPopover.swift; sourceTree = "<group>"; };
|
||||
4A71C0062F40300100A17E01 /* NativeAttachmentPopoverCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeAttachmentPopoverCoordinator.swift; sourceTree = "<group>"; };
|
||||
4A71C0082F40400100A17E01 /* ConcentricSheetSurface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcentricSheetSurface.swift; sourceTree = "<group>"; };
|
||||
331C809C294A618700263BE5 /* UIKitEncoded.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = UIKitEncoded.png; sourceTree = "<group>"; };
|
||||
331C809E294A618700263BE5 /* UIKitEncoded.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = UIKitEncoded.jpg; sourceTree = "<group>"; };
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
42F36AA401015EC51F984FEA /* 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 = "<group>"; };
|
||||
529B9B84AD8748153EB60BCF /* 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 = "<group>"; };
|
||||
57A155722F02B92C397E5AE2 /* 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 = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
7CF2415588E96D5723581BA9 /* 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 = "<group>"; };
|
||||
8906419FB4E98B4B12B7A56F /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
8EB101B7046447A0502FE6C9 /* 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 = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
CD6B899582D0416ADBD8A68F /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
3C28C6B702C81085E6F96F2A /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
42C129326CE4E1B8E617B9CD /* Pods_RunnerTests.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
33ADD70AB275E0EC81295559 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */,
|
||||
331C80A0294A618700263BE5 /* Fixtures */,
|
||||
);
|
||||
path = RunnerTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
331C80A0294A618700263BE5 /* Fixtures */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
331C809E294A618700263BE5 /* UIKitEncoded.jpg */,
|
||||
331C809C294A618700263BE5 /* UIKitEncoded.png */,
|
||||
);
|
||||
path = Fixtures;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
746EA2D40EB8F35E67C48311 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
57A155722F02B92C397E5AE2 /* Pods-Runner.debug.xcconfig */,
|
||||
30CE81D3D1E0B195EF2A6390 /* Pods-Runner.release.xcconfig */,
|
||||
8EB101B7046447A0502FE6C9 /* Pods-Runner.profile.xcconfig */,
|
||||
42F36AA401015EC51F984FEA /* Pods-RunnerTests.debug.xcconfig */,
|
||||
7CF2415588E96D5723581BA9 /* Pods-RunnerTests.release.xcconfig */,
|
||||
529B9B84AD8748153EB60BCF /* Pods-RunnerTests.profile.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||
);
|
||||
name = Flutter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146E51CF9000F007C117D = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9740EEB11CF90186004384FC /* Flutter */,
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
746EA2D40EB8F35E67C48311 /* Pods */,
|
||||
D12A84F9A6072498822784CC /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146EF1CF9000F007C117D /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||
97C147021CF9000F007C117D /* Info.plist */,
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||
331C809A294A618700263BE5 /* MediaSanitizer.swift */,
|
||||
4A71C0022F40100100A17E01 /* InlinePhotoPicker.swift */,
|
||||
4A71C0042F40200100A17E01 /* NativeAttachmentPopover.swift */,
|
||||
4A71C0062F40300100A17E01 /* NativeAttachmentPopoverCoordinator.swift */,
|
||||
4A71C0082F40400100A17E01 /* ConcentricSheetSurface.swift */,
|
||||
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D12A84F9A6072498822784CC /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
8906419FB4E98B4B12B7A56F /* Pods_Runner.framework */,
|
||||
CD6B899582D0416ADBD8A68F /* Pods_RunnerTests.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
29C475F38B9F5B6D27BD900B /* [CP] Check Pods Manifest.lock */,
|
||||
331C807D294A63A400263BE5 /* Sources */,
|
||||
331C807F294A63A400263BE5 /* Resources */,
|
||||
3C28C6B702C81085E6F96F2A /* Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
331C8086294A63A400263BE5 /* PBXTargetDependency */,
|
||||
);
|
||||
name = RunnerTests;
|
||||
productName = RunnerTests;
|
||||
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
1D327B920BBA7EF4545E2A92 /* [CP] Check Pods Manifest.lock */,
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
E0B5862D106D142B580309AF /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
331C8080294A63A400263BE5 = {
|
||||
CreatedOnToolsVersion = 14.0;
|
||||
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||
};
|
||||
97C146ED1CF9000F007C117D = {
|
||||
CreatedOnToolsVersion = 7.3.1;
|
||||
LastSwiftMigration = 1100;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 97C146E51CF9000F007C117D;
|
||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
97C146ED1CF9000F007C117D /* Runner */,
|
||||
331C8080294A63A400263BE5 /* RunnerTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
331C807F294A63A400263BE5 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
331C809F294A63AB00263BE5 /* UIKitEncoded.jpg in Resources */,
|
||||
331C809D294A63AB00263BE5 /* UIKitEncoded.png in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
1D327B920BBA7EF4545E2A92 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-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";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
29C475F38B9F5B6D27BD900B /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||
);
|
||||
name = "Thin Binary";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run Script";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
E0B5862D106D142B580309AF /* [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 */
|
||||
331C807D294A63A400263BE5 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
331C809B294A63AB00263BE5 /* MediaSanitizer.swift in Sources */,
|
||||
4A71C0012F40100100A17E01 /* InlinePhotoPicker.swift in Sources */,
|
||||
4A71C0032F40200100A17E01 /* NativeAttachmentPopover.swift in Sources */,
|
||||
4A71C0052F40300100A17E01 /* NativeAttachmentPopoverCoordinator.swift in Sources */,
|
||||
4A71C0072F40400100A17E01 /* ConcentricSheetSurface.swift in Sources */,
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C146FB1CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C147001CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
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;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = Buzz;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
331C8088294A63A400263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 42F36AA401015EC51F984FEA /* Pods-RunnerTests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMPRESS_PNG_FILES = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
STRIP_PNG_TEXT = NO;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER).RunnerTests";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Buzz.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Buzz";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
331C8089294A63A400263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7CF2415588E96D5723581BA9 /* Pods-RunnerTests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMPRESS_PNG_FILES = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
STRIP_PNG_TEXT = NO;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER).RunnerTests";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Buzz.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Buzz";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
331C808A294A63A400263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 529B9B84AD8748153EB60BCF /* Pods-RunnerTests.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMPRESS_PNG_FILES = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
STRIP_PNG_TEXT = NO;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER).RunnerTests";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Buzz.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Buzz";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
97C147031CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
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;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147041CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
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;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
97C147061CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = Buzz;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147071CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = Buzz;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
331C8088294A63A400263BE5 /* Debug */,
|
||||
331C8089294A63A400263BE5 /* Release */,
|
||||
331C808A294A63A400263BE5 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147031CF9000F007C117D /* Debug */,
|
||||
97C147041CF9000F007C117D /* Release */,
|
||||
249021D3217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147061CF9000F007C117D /* Debug */,
|
||||
97C147071CF9000F007C117D /* Release */,
|
||||
249021D4217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Buzz.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Buzz.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "331C8080294A63A400263BE5"
|
||||
BuildableName = "RunnerTests.xctest"
|
||||
BlueprintName = "RunnerTests"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Buzz.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Profile"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Buzz.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,586 @@
|
||||
import AVFoundation
|
||||
import Flutter
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||
private var mediaUploadChannel: FlutterMethodChannel?
|
||||
private var qrScannerChannel: FlutterMethodChannel?
|
||||
private var inlinePhotoPickerSupportChannel: FlutterMethodChannel?
|
||||
private var concentricSheetSurfaceChannel: FlutterMethodChannel?
|
||||
private var nativeAttachmentPopoverCoordinator: NativeAttachmentPopoverCoordinator?
|
||||
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.badge]) { _, _ in }
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||
let messenger = engineBridge.applicationRegistrar.messenger()
|
||||
mediaUploadChannel = FlutterMethodChannel(
|
||||
name: "buzz/media_upload",
|
||||
binaryMessenger: messenger
|
||||
)
|
||||
mediaUploadChannel?.setMethodCallHandler { [weak self] call, result in
|
||||
self?.handleMediaUploadMethodCall(call, result: result)
|
||||
}
|
||||
qrScannerChannel = FlutterMethodChannel(
|
||||
name: "buzz/qr_scanner",
|
||||
binaryMessenger: messenger
|
||||
)
|
||||
qrScannerChannel?.setMethodCallHandler { call, result in
|
||||
Self.handleQrScannerMethodCall(call, result: result)
|
||||
}
|
||||
inlinePhotoPickerSupportChannel = FlutterMethodChannel(
|
||||
name: "buzz/inline_photo_picker",
|
||||
binaryMessenger: messenger
|
||||
)
|
||||
inlinePhotoPickerSupportChannel?.setMethodCallHandler { call, result in
|
||||
guard call.method == "isSupported" else {
|
||||
result(FlutterMethodNotImplemented)
|
||||
return
|
||||
}
|
||||
if #available(iOS 17.0, *) {
|
||||
result(true)
|
||||
} else {
|
||||
result(false)
|
||||
}
|
||||
}
|
||||
|
||||
if let inlinePhotoPickerRegistrar = engineBridge.pluginRegistry.registrar(
|
||||
forPlugin: "BuzzInlinePhotoPicker"
|
||||
) {
|
||||
inlinePhotoPickerRegistrar.register(
|
||||
InlinePhotoPickerFactory(
|
||||
messenger: messenger,
|
||||
parentViewController: inlinePhotoPickerRegistrar.viewController
|
||||
),
|
||||
withId: "buzz/inline_photo_picker"
|
||||
)
|
||||
}
|
||||
|
||||
if let concentricSheetRegistrar = engineBridge.pluginRegistry.registrar(
|
||||
forPlugin: "BuzzConcentricSheetSurface"
|
||||
) {
|
||||
concentricSheetRegistrar.register(
|
||||
ConcentricSheetSurfaceFactory(),
|
||||
withId: "buzz/concentric_sheet_surface"
|
||||
)
|
||||
concentricSheetSurfaceChannel = FlutterMethodChannel(
|
||||
name: "buzz/concentric_sheet_surface",
|
||||
binaryMessenger: messenger
|
||||
)
|
||||
concentricSheetSurfaceChannel?.setMethodCallHandler { call, result in
|
||||
guard call.method == "isSupported" else {
|
||||
result(FlutterMethodNotImplemented)
|
||||
return
|
||||
}
|
||||
if #available(iOS 26.0, *) {
|
||||
result(true)
|
||||
} else {
|
||||
result(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let nativeAttachmentRegistrar = engineBridge.pluginRegistry.registrar(
|
||||
forPlugin: "BuzzNativeAttachmentPopover"
|
||||
)
|
||||
nativeAttachmentPopoverCoordinator = NativeAttachmentPopoverCoordinator(
|
||||
messenger: messenger,
|
||||
parentViewController: nativeAttachmentRegistrar?.viewController
|
||||
)
|
||||
}
|
||||
|
||||
private static func handleQrScannerMethodCall(
|
||||
_ call: FlutterMethodCall,
|
||||
result: @escaping FlutterResult
|
||||
) {
|
||||
switch call.method {
|
||||
case "usesDynamicIslandQrScannerPortal":
|
||||
result(
|
||||
UIDevice.current.userInterfaceIdiom == .phone
|
||||
&& usesDynamicIslandQrScannerPortal(
|
||||
safeAreaTopInset: activeWindowSafeAreaTopInset()
|
||||
)
|
||||
)
|
||||
case "setDynamicIslandScannerStatusBarHidden":
|
||||
guard let hidden = call.arguments as? Bool else {
|
||||
result(
|
||||
FlutterError(
|
||||
code: "invalid_arguments",
|
||||
message: "Expected a Bool status-bar visibility value.",
|
||||
details: nil
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
UIApplication.shared.setStatusBarHidden(hidden, with: .fade)
|
||||
result(nil)
|
||||
case "performDynamicIslandQrScanSuccessHaptic":
|
||||
let generator = UINotificationFeedbackGenerator()
|
||||
generator.prepare()
|
||||
generator.notificationOccurred(.success)
|
||||
result(nil)
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
|
||||
static func usesDynamicIslandQrScannerPortal(
|
||||
safeAreaTopInset: CGFloat
|
||||
) -> Bool {
|
||||
safeAreaTopInset > 50
|
||||
}
|
||||
|
||||
private static func activeWindowSafeAreaTopInset() -> CGFloat {
|
||||
UIApplication.shared.connectedScenes
|
||||
.compactMap { $0 as? UIWindowScene }
|
||||
.filter { $0.activationState == .foregroundActive }
|
||||
.flatMap(\.windows)
|
||||
.first(where: \.isKeyWindow)?
|
||||
.safeAreaInsets.top ?? 0
|
||||
}
|
||||
|
||||
private func handleMediaUploadMethodCall(
|
||||
_ call: FlutterMethodCall,
|
||||
result: @escaping FlutterResult
|
||||
) {
|
||||
switch call.method {
|
||||
case "sanitizeImageForUpload":
|
||||
guard
|
||||
let arguments = call.arguments as? [String: Any],
|
||||
let typedData = arguments["bytes"] as? FlutterStandardTypedData,
|
||||
let mimeType = arguments["mimeType"] as? String
|
||||
else {
|
||||
result(
|
||||
FlutterError(
|
||||
code: "invalid_arguments",
|
||||
message: "Expected image bytes and mime type.",
|
||||
details: nil
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
guard let image = UIImage(data: typedData.data) else {
|
||||
result(
|
||||
FlutterError(
|
||||
code: "sanitize_failed",
|
||||
message: "Unable to decode picked image.",
|
||||
details: nil
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
guard let sanitizedData = try MediaSanitizer.sanitizeImage(image, mimeType: mimeType) else {
|
||||
result(
|
||||
FlutterError(
|
||||
code: "sanitize_failed",
|
||||
message: "Unable to sanitize picked image.",
|
||||
details: mimeType
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
result(FlutterStandardTypedData(bytes: sanitizedData))
|
||||
} catch {
|
||||
result(
|
||||
FlutterError(
|
||||
code: "sanitize_failed",
|
||||
message: "Unable to sanitize picked image.",
|
||||
details: mimeType
|
||||
)
|
||||
)
|
||||
}
|
||||
case "transcodeImageToJpeg":
|
||||
guard let typedData = call.arguments as? FlutterStandardTypedData else {
|
||||
result(
|
||||
FlutterError(
|
||||
code: "invalid_arguments",
|
||||
message: "Expected raw image bytes.",
|
||||
details: nil
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
guard let image = UIImage(data: typedData.data) else {
|
||||
result(
|
||||
FlutterError(
|
||||
code: "transcode_failed",
|
||||
message: "Unable to convert picked image to JPEG.",
|
||||
details: nil
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
guard let jpegData = try MediaSanitizer.encodeJpeg(image) else {
|
||||
result(
|
||||
FlutterError(
|
||||
code: "transcode_failed",
|
||||
message: "Unable to convert picked image to JPEG.",
|
||||
details: nil
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
result(FlutterStandardTypedData(bytes: jpegData))
|
||||
} catch {
|
||||
result(
|
||||
FlutterError(
|
||||
code: "transcode_failed",
|
||||
message: "Unable to convert picked image to JPEG.",
|
||||
details: nil
|
||||
)
|
||||
)
|
||||
}
|
||||
case "transcodeVideoToMp4":
|
||||
guard let sourcePath = call.arguments as? String else {
|
||||
result(
|
||||
FlutterError(
|
||||
code: "invalid_arguments",
|
||||
message: "Expected source file path as String.",
|
||||
details: nil
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
transcodeVideoToMp4(sourcePath: sourcePath, result: result)
|
||||
case "generateVideoPoster":
|
||||
guard let sourcePath = call.arguments as? String else {
|
||||
result(
|
||||
FlutterError(
|
||||
code: "invalid_arguments",
|
||||
message: "Expected source file path as String.",
|
||||
details: nil
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
generateVideoPoster(sourcePath: sourcePath, result: result)
|
||||
case "clipboardHasImage":
|
||||
result(UIPasteboard.general.hasImages)
|
||||
case "readClipboardImage":
|
||||
guard let imageData = Self.clipboardImageData(from: UIPasteboard.general) else {
|
||||
result(nil)
|
||||
return
|
||||
}
|
||||
result(FlutterStandardTypedData(bytes: imageData))
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
|
||||
static func clipboardImageData(from pasteboard: UIPasteboard) -> Data? {
|
||||
if let pngData = pasteboard.data(forPasteboardType: "public.png") {
|
||||
return pngData
|
||||
}
|
||||
if let jpegData = pasteboard.data(forPasteboardType: "public.jpeg") {
|
||||
return jpegData
|
||||
}
|
||||
for imageType in ["public.heic", "public.heif", "org.webmproject.webp", "com.compuserve.gif"] {
|
||||
if let imageData = pasteboard.data(forPasteboardType: imageType) {
|
||||
return imageData
|
||||
}
|
||||
}
|
||||
guard let image = pasteboard.image else {
|
||||
return nil
|
||||
}
|
||||
return image.pngData()
|
||||
}
|
||||
|
||||
private func transcodeVideoToMp4(
|
||||
sourcePath: String,
|
||||
result: @escaping FlutterResult
|
||||
) {
|
||||
let sourceURL = URL(fileURLWithPath: sourcePath)
|
||||
let asset = AVURLAsset(url: sourceURL)
|
||||
|
||||
// Do not export the source asset directly. An iPhone video can carry GPS,
|
||||
// spatial-video, and other data tracks even when its user-visible metadata
|
||||
// is cleared. A fresh composition copies only one video and one audio
|
||||
// track, so those private channels cannot reach the relay.
|
||||
let composition = AVMutableComposition()
|
||||
guard
|
||||
let sourceVideo = asset.tracks(withMediaType: .video).first,
|
||||
let destinationVideo = composition.addMutableTrack(
|
||||
withMediaType: .video,
|
||||
preferredTrackID: kCMPersistentTrackID_Invalid
|
||||
)
|
||||
else {
|
||||
result(
|
||||
FlutterError(
|
||||
code: "transcode_failed",
|
||||
message: "The selected file does not contain a video track.",
|
||||
details: nil
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let sourceAudio = asset.tracks(withMediaType: .audio).first
|
||||
let insertionTimes = Self.relativeTrackInsertionTimes(
|
||||
videoStart: sourceVideo.timeRange.start,
|
||||
audioStart: sourceAudio?.timeRange.start
|
||||
)
|
||||
try destinationVideo.insertTimeRange(
|
||||
sourceVideo.timeRange,
|
||||
of: sourceVideo,
|
||||
at: insertionTimes.video
|
||||
)
|
||||
destinationVideo.preferredTransform = sourceVideo.preferredTransform
|
||||
|
||||
if
|
||||
let sourceAudio,
|
||||
let destinationAudio = composition.addMutableTrack(
|
||||
withMediaType: .audio,
|
||||
preferredTrackID: kCMPersistentTrackID_Invalid
|
||||
)
|
||||
{
|
||||
try destinationAudio.insertTimeRange(
|
||||
sourceAudio.timeRange,
|
||||
of: sourceAudio,
|
||||
at: insertionTimes.audio ?? .zero
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
result(
|
||||
FlutterError(
|
||||
code: "transcode_failed",
|
||||
message: error.localizedDescription,
|
||||
details: nil
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
guard
|
||||
let exportSession = AVAssetExportSession(
|
||||
asset: composition,
|
||||
// Passthrough preserves the source's HEVC codec and container
|
||||
// metadata. Buzz accepts only canonical H.264/AAC MP4s with no
|
||||
// metadata channels, so re-encode instead of copying the movie.
|
||||
presetName: AVAssetExportPresetMediumQuality
|
||||
)
|
||||
else {
|
||||
result(
|
||||
FlutterError(
|
||||
code: "transcode_failed",
|
||||
message: "Unable to create export session.",
|
||||
details: nil
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
let outputURL = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent(UUID().uuidString)
|
||||
.appendingPathExtension("mp4")
|
||||
|
||||
exportSession.outputURL = outputURL
|
||||
exportSession.outputFileType = .mp4
|
||||
exportSession.shouldOptimizeForNetworkUse = true
|
||||
// `forSharing()` intentionally retains playback metadata. The relay
|
||||
// rejects every descriptive metadata channel to avoid leaking location or
|
||||
// other private information, so write no source metadata at all.
|
||||
exportSession.metadata = []
|
||||
exportSession.metadataItemFilter = nil
|
||||
|
||||
exportSession.exportAsynchronously {
|
||||
switch exportSession.status {
|
||||
case .completed:
|
||||
do {
|
||||
// AVFoundation writes a standard sample-dependency table (`sdtp`).
|
||||
// Older Buzz relays mistook that playback-only box for metadata. Keep
|
||||
// its size and payload in a `free` box so chunk offsets stay valid and
|
||||
// uploads work before those relays receive the validator fix.
|
||||
try Self.neutralizeSampleDependencyBoxes(at: outputURL)
|
||||
result(outputURL.path)
|
||||
} catch {
|
||||
try? FileManager.default.removeItem(at: outputURL)
|
||||
result(
|
||||
FlutterError(
|
||||
code: "transcode_failed",
|
||||
message: "Unable to canonicalize transcoded video.",
|
||||
details: error.localizedDescription
|
||||
)
|
||||
)
|
||||
}
|
||||
default:
|
||||
let errorMessage =
|
||||
exportSession.error?.localizedDescription
|
||||
?? "Video transcoding failed with status \(exportSession.status.rawValue)."
|
||||
result(
|
||||
FlutterError(
|
||||
code: "transcode_failed",
|
||||
message: errorMessage,
|
||||
details: nil
|
||||
)
|
||||
)
|
||||
// Clean up partial output on failure.
|
||||
try? FileManager.default.removeItem(at: outputURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func relativeTrackInsertionTimes(
|
||||
videoStart: CMTime,
|
||||
audioStart: CMTime?
|
||||
) -> (video: CMTime, audio: CMTime?) {
|
||||
guard let audioStart else {
|
||||
return (video: .zero, audio: nil)
|
||||
}
|
||||
|
||||
let timelineStart =
|
||||
CMTimeCompare(audioStart, videoStart) < 0 ? audioStart : videoStart
|
||||
return (
|
||||
video: CMTimeSubtract(videoStart, timelineStart),
|
||||
audio: CMTimeSubtract(audioStart, timelineStart)
|
||||
)
|
||||
}
|
||||
|
||||
private func generateVideoPoster(
|
||||
sourcePath: String,
|
||||
result: @escaping FlutterResult
|
||||
) {
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let asset = AVURLAsset(url: URL(fileURLWithPath: sourcePath))
|
||||
let generator = AVAssetImageGenerator(asset: asset)
|
||||
generator.appliesPreferredTrackTransform = true
|
||||
generator.maximumSize = CGSize(width: 720, height: 720)
|
||||
generator.requestedTimeToleranceBefore = .positiveInfinity
|
||||
generator.requestedTimeToleranceAfter = .positiveInfinity
|
||||
|
||||
do {
|
||||
let durationSeconds = CMTimeGetSeconds(asset.duration)
|
||||
let middleTime = durationSeconds.isFinite && durationSeconds > 0
|
||||
? min(durationSeconds / 2, 1)
|
||||
: 0
|
||||
let candidateTimes = [0, 0.1, middleTime]
|
||||
var posterImage: CGImage?
|
||||
var lastError: Error?
|
||||
|
||||
for seconds in candidateTimes {
|
||||
do {
|
||||
posterImage = try generator.copyCGImage(
|
||||
at: CMTime(seconds: seconds, preferredTimescale: 600),
|
||||
actualTime: nil
|
||||
)
|
||||
if posterImage != nil { break }
|
||||
} catch {
|
||||
lastError = error
|
||||
}
|
||||
}
|
||||
|
||||
guard let posterImage else {
|
||||
throw lastError ?? NSError(
|
||||
domain: "BuzzVideoPoster",
|
||||
code: 1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Unable to decode a video frame."]
|
||||
)
|
||||
}
|
||||
guard let jpegData = try MediaSanitizer.encodeJpeg(UIImage(cgImage: posterImage)) else {
|
||||
throw NSError(
|
||||
domain: "BuzzVideoPoster",
|
||||
code: 2,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Unable to encode video poster."]
|
||||
)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
result(FlutterStandardTypedData(bytes: jpegData))
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
result(
|
||||
FlutterError(
|
||||
code: "poster_failed",
|
||||
message: "Unable to create a video preview.",
|
||||
details: error.localizedDescription
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func neutralizeSampleDependencyBoxes(at url: URL) throws {
|
||||
var data = try Data(contentsOf: url)
|
||||
try neutralizeSampleDependencyBoxes(in: &data, start: 0, end: data.count)
|
||||
try data.write(to: url, options: .atomic)
|
||||
}
|
||||
|
||||
private static func neutralizeSampleDependencyBoxes(
|
||||
in data: inout Data,
|
||||
start: Int,
|
||||
end: Int
|
||||
) throws {
|
||||
let containers: Set<[UInt8]> = [
|
||||
Array("moov".utf8), Array("trak".utf8), Array("mdia".utf8),
|
||||
Array("minf".utf8), Array("stbl".utf8), Array("edts".utf8),
|
||||
Array("dinf".utf8), Array("sinf".utf8), Array("schi".utf8),
|
||||
]
|
||||
let sampleDependencyType = Array("sdtp".utf8)
|
||||
let freeType = Array("free".utf8)
|
||||
var offset = start
|
||||
|
||||
while offset < end {
|
||||
guard end - offset >= 8 else { throw invalidMp4BoxError() }
|
||||
let compactSize = Int(readBigEndianUInt32(data, at: offset))
|
||||
var headerSize = 8
|
||||
let boxSize: Int
|
||||
if compactSize == 1 {
|
||||
guard end - offset >= 16 else { throw invalidMp4BoxError() }
|
||||
let extendedSize = readBigEndianUInt64(data, at: offset + 8)
|
||||
guard extendedSize <= UInt64(Int.max) else { throw invalidMp4BoxError() }
|
||||
boxSize = Int(extendedSize)
|
||||
headerSize = 16
|
||||
} else if compactSize == 0 {
|
||||
boxSize = end - offset
|
||||
} else {
|
||||
boxSize = compactSize
|
||||
}
|
||||
|
||||
guard boxSize >= headerSize, offset + boxSize <= end else {
|
||||
throw invalidMp4BoxError()
|
||||
}
|
||||
let type = Array(data[(offset + 4)..<(offset + 8)])
|
||||
if type == sampleDependencyType {
|
||||
data.replaceSubrange((offset + 4)..<(offset + 8), with: freeType)
|
||||
} else if containers.contains(type) {
|
||||
try neutralizeSampleDependencyBoxes(
|
||||
in: &data,
|
||||
start: offset + headerSize,
|
||||
end: offset + boxSize
|
||||
)
|
||||
}
|
||||
offset += boxSize
|
||||
}
|
||||
}
|
||||
|
||||
private static func readBigEndianUInt32(_ data: Data, at offset: Int) -> UInt32 {
|
||||
data[offset..<(offset + 4)].reduce(0) { ($0 << 8) | UInt32($1) }
|
||||
}
|
||||
|
||||
private static func readBigEndianUInt64(_ data: Data, at offset: Int) -> UInt64 {
|
||||
data[offset..<(offset + 8)].reduce(0) { ($0 << 8) | UInt64($1) }
|
||||
}
|
||||
|
||||
private static func invalidMp4BoxError() -> NSError {
|
||||
NSError(
|
||||
domain: "BuzzVideoTranscode",
|
||||
code: 1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Invalid MP4 box structure."]
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "Icon-App-1024x1024@1x.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 601 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 918 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,5 @@
|
||||
# Launch Screen Assets
|
||||
|
||||
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
|
||||
|
||||
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
|
||||
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0" green="0" blue="0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchImage" width="168" height="168"/>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Flutter View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -0,0 +1,54 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
|
||||
final class ConcentricSheetSurfaceFactory: NSObject, FlutterPlatformViewFactory {
|
||||
func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
|
||||
FlutterStandardMessageCodec.sharedInstance()
|
||||
}
|
||||
|
||||
func create(
|
||||
withFrame frame: CGRect,
|
||||
viewIdentifier viewId: Int64,
|
||||
arguments args: Any?
|
||||
) -> FlutterPlatformView {
|
||||
ConcentricSheetSurfacePlatformView(frame: frame, arguments: args)
|
||||
}
|
||||
}
|
||||
|
||||
final class ConcentricSheetSurfacePlatformView: NSObject, FlutterPlatformView {
|
||||
private let surfaceView: UIView
|
||||
|
||||
init(frame: CGRect, arguments args: Any?) {
|
||||
let arguments = args as? [String: Any]
|
||||
let colorValue = (arguments?["color"] as? NSNumber)?.uint32Value ?? 0xFFFFFFFF
|
||||
let minimumRadius = (arguments?["minimumRadius"] as? NSNumber)?.doubleValue ?? 24
|
||||
|
||||
surfaceView = UIView(frame: frame)
|
||||
surfaceView.isOpaque = true
|
||||
surfaceView.backgroundColor = Self.color(from: colorValue)
|
||||
surfaceView.clipsToBounds = true
|
||||
surfaceView.layer.cornerCurve = .continuous
|
||||
|
||||
if #available(iOS 26.0, *) {
|
||||
surfaceView.cornerConfiguration = .uniformCorners(
|
||||
radius: .containerConcentric(minimum: minimumRadius)
|
||||
)
|
||||
} else {
|
||||
surfaceView.layer.cornerRadius = minimumRadius
|
||||
}
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
func view() -> UIView {
|
||||
surfaceView
|
||||
}
|
||||
|
||||
private static func color(from value: UInt32) -> UIColor {
|
||||
let alpha = CGFloat((value >> 24) & 0xFF) / 255
|
||||
let red = CGFloat((value >> 16) & 0xFF) / 255
|
||||
let green = CGFloat((value >> 8) & 0xFF) / 255
|
||||
let blue = CGFloat(value & 0xFF) / 255
|
||||
return UIColor(red: red, green: green, blue: blue, alpha: alpha)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>$(APP_DISPLAY_NAME)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Buzz</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.buzz.deeplink</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>buzz</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>FlutterDeepLinkingEnabled</key>
|
||||
<false/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Buzz needs camera access so you can take photos to attach to messages and scan QR codes for device pairing.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Buzz needs photo library access so you can attach images to messages.</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>Buzz needs permission to save images to your photo library.</string>
|
||||
<key>PHPhotoLibraryPreventAutomaticLimitedAccessAlert</key>
|
||||
<true/>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneClassName</key>
|
||||
<string>UIWindowScene</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>flutter</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,237 @@
|
||||
import Flutter
|
||||
import PhotosUI
|
||||
import UIKit
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
final class InlinePhotoPickerFactory: NSObject, FlutterPlatformViewFactory {
|
||||
private let messenger: FlutterBinaryMessenger
|
||||
private weak var parentViewController: UIViewController?
|
||||
|
||||
init(
|
||||
messenger: FlutterBinaryMessenger,
|
||||
parentViewController: UIViewController?
|
||||
) {
|
||||
self.messenger = messenger
|
||||
self.parentViewController = parentViewController
|
||||
super.init()
|
||||
}
|
||||
|
||||
func create(
|
||||
withFrame frame: CGRect,
|
||||
viewIdentifier viewId: Int64,
|
||||
arguments args: Any?
|
||||
) -> FlutterPlatformView {
|
||||
InlinePhotoPickerPlatformView(
|
||||
frame: frame,
|
||||
viewIdentifier: viewId,
|
||||
messenger: messenger,
|
||||
parentViewController: parentViewController
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
final class InlinePhotoPickerPlatformView: NSObject, FlutterPlatformView {
|
||||
private let containerView: UIView
|
||||
private let channel: FlutterMethodChannel
|
||||
private weak var parentViewController: UIViewController?
|
||||
private var pickerViewController: PHPickerViewController?
|
||||
private var selectionGeneration = 0
|
||||
private var selectionTask: Task<Void, Never>?
|
||||
private var selectedTemporaryPaths: [String] = []
|
||||
|
||||
init(
|
||||
frame: CGRect,
|
||||
viewIdentifier viewId: Int64,
|
||||
messenger: FlutterBinaryMessenger,
|
||||
parentViewController: UIViewController?
|
||||
) {
|
||||
containerView = UIView(frame: frame)
|
||||
channel = FlutterMethodChannel(
|
||||
name: "buzz/inline_photo_picker/\(viewId)",
|
||||
binaryMessenger: messenger
|
||||
)
|
||||
self.parentViewController = parentViewController
|
||||
super.init()
|
||||
|
||||
channel.setMethodCallHandler { [weak self] call, result in
|
||||
guard call.method == "claimSelection" else {
|
||||
result(FlutterMethodNotImplemented)
|
||||
return
|
||||
}
|
||||
let paths = call.arguments as? [String] ?? []
|
||||
guard let self, paths == self.selectedTemporaryPaths else {
|
||||
result(false)
|
||||
return
|
||||
}
|
||||
self.selectedTemporaryPaths = []
|
||||
result(true)
|
||||
}
|
||||
|
||||
containerView.backgroundColor = .clear
|
||||
containerView.clipsToBounds = true
|
||||
if #available(iOS 17.0, *) {
|
||||
installPicker()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
selectionTask?.cancel()
|
||||
Self.removeTemporaryFiles(selectedTemporaryPaths)
|
||||
channel.setMethodCallHandler(nil)
|
||||
pickerViewController?.willMove(toParent: nil)
|
||||
pickerViewController?.view.removeFromSuperview()
|
||||
pickerViewController?.removeFromParent()
|
||||
}
|
||||
|
||||
func view() -> UIView {
|
||||
containerView
|
||||
}
|
||||
|
||||
@available(iOS 17.0, *)
|
||||
private func installPicker() {
|
||||
var configuration = PHPickerConfiguration(photoLibrary: .shared())
|
||||
configuration.filter = .images
|
||||
configuration.selectionLimit = 0
|
||||
configuration.selection = .continuousAndOrdered
|
||||
configuration.preferredAssetRepresentationMode = .compatible
|
||||
configuration.disabledCapabilities = [
|
||||
.search,
|
||||
.stagingArea,
|
||||
.collectionNavigation,
|
||||
.selectionActions,
|
||||
]
|
||||
configuration.edgesWithoutContentMargins = .all
|
||||
|
||||
let picker = PHPickerViewController(configuration: configuration)
|
||||
picker.delegate = self
|
||||
picker.view.backgroundColor = .clear
|
||||
picker.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
if let parentViewController {
|
||||
parentViewController.addChild(picker)
|
||||
}
|
||||
containerView.addSubview(picker.view)
|
||||
NSLayoutConstraint.activate([
|
||||
picker.view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
|
||||
picker.view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
|
||||
picker.view.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||
picker.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
||||
])
|
||||
if parentViewController != nil {
|
||||
picker.didMove(toParent: parentViewController)
|
||||
}
|
||||
pickerViewController = picker
|
||||
containerView.layoutIfNeeded()
|
||||
}
|
||||
|
||||
private func exportPickerResult(_ result: PHPickerResult) async throws -> String {
|
||||
let provider = result.itemProvider
|
||||
guard
|
||||
let typeIdentifier = provider.registeredTypeIdentifiers.first(where: {
|
||||
guard let type = UTType($0) else { return false }
|
||||
return type.conforms(to: .image)
|
||||
})
|
||||
else {
|
||||
throw InlinePhotoPickerError.unsupportedImage
|
||||
}
|
||||
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
provider.loadFileRepresentation(forTypeIdentifier: typeIdentifier) {
|
||||
sourceURL,
|
||||
error in
|
||||
if let error {
|
||||
continuation.resume(throwing: error)
|
||||
return
|
||||
}
|
||||
guard let sourceURL else {
|
||||
continuation.resume(throwing: InlinePhotoPickerError.missingFile)
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let fileExtension =
|
||||
sourceURL.pathExtension.isEmpty
|
||||
? (UTType(typeIdentifier)?.preferredFilenameExtension ?? "jpg")
|
||||
: sourceURL.pathExtension
|
||||
let destinationURL = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent(UUID().uuidString)
|
||||
.appendingPathExtension(fileExtension)
|
||||
try FileManager.default.copyItem(
|
||||
at: sourceURL,
|
||||
to: destinationURL
|
||||
)
|
||||
continuation.resume(returning: destinationURL.path)
|
||||
} catch {
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func removeTemporaryFiles(_ paths: [String]) {
|
||||
for path in paths where !path.isEmpty {
|
||||
try? FileManager.default.removeItem(atPath: path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension InlinePhotoPickerPlatformView: PHPickerViewControllerDelegate {
|
||||
func picker(
|
||||
_ picker: PHPickerViewController,
|
||||
didFinishPicking results: [PHPickerResult]
|
||||
) {
|
||||
selectionGeneration += 1
|
||||
let generation = selectionGeneration
|
||||
selectionTask?.cancel()
|
||||
selectionTask = nil
|
||||
Self.removeTemporaryFiles(selectedTemporaryPaths)
|
||||
selectedTemporaryPaths = []
|
||||
channel.invokeMethod(
|
||||
"selectionCountChanged",
|
||||
arguments: results.count
|
||||
)
|
||||
|
||||
guard !results.isEmpty else {
|
||||
channel.invokeMethod("selectionDidChange", arguments: [String]())
|
||||
return
|
||||
}
|
||||
|
||||
selectionTask = Task { [weak self] in
|
||||
guard let self else { return }
|
||||
var paths: [String] = []
|
||||
do {
|
||||
for result in results {
|
||||
try Task.checkCancellation()
|
||||
paths.append(try await self.exportPickerResult(result))
|
||||
}
|
||||
try Task.checkCancellation()
|
||||
await MainActor.run {
|
||||
guard generation == self.selectionGeneration else {
|
||||
Self.removeTemporaryFiles(paths)
|
||||
return
|
||||
}
|
||||
self.selectedTemporaryPaths = paths
|
||||
self.selectionTask = nil
|
||||
self.channel.invokeMethod("selectionDidChange", arguments: paths)
|
||||
}
|
||||
} catch is CancellationError {
|
||||
Self.removeTemporaryFiles(paths)
|
||||
} catch {
|
||||
Self.removeTemporaryFiles(paths)
|
||||
await MainActor.run {
|
||||
guard generation == self.selectionGeneration else { return }
|
||||
self.selectionTask = nil
|
||||
self.channel.invokeMethod(
|
||||
"didFail",
|
||||
arguments: "Unable to prepare the selected photos."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum InlinePhotoPickerError: Error {
|
||||
case missingFile
|
||||
case unsupportedImage
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
private enum MediaSanitizationError: Error {
|
||||
case invalidPng
|
||||
case invalidJpeg
|
||||
}
|
||||
|
||||
enum MediaSanitizer {
|
||||
private static let pngSignature = Data([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])
|
||||
private static let allowedPngAncillaryChunks: Set<String> = [
|
||||
"cHRM", "gAMA", "sBIT", "sRGB", "bKGD", "hIST", "tRNS", "sPLT", "acTL", "fcTL", "fdAT",
|
||||
]
|
||||
|
||||
static func sanitizeImage(_ image: UIImage, mimeType: String) throws -> Data? {
|
||||
switch mimeType {
|
||||
case "image/png":
|
||||
guard let image = renderInSRGB(image), let encoded = image.pngData() else { return nil }
|
||||
return try scrubPng(encoded)
|
||||
case "image/jpeg":
|
||||
return try encodeJpeg(image)
|
||||
case "image/webp":
|
||||
guard let image = renderInSRGB(image), let encoded = image.pngData() else { return nil }
|
||||
return try scrubPng(encoded)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
static func encodeJpeg(_ image: UIImage) throws -> Data? {
|
||||
guard
|
||||
let image = renderInSRGB(image),
|
||||
let encoded = image.jpegData(compressionQuality: 1.0)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
return try scrubJpeg(encoded)
|
||||
}
|
||||
|
||||
private static func renderInSRGB(_ image: UIImage) -> UIImage? {
|
||||
guard image.size.width > 0, image.size.height > 0 else { return nil }
|
||||
let format = UIGraphicsImageRendererFormat()
|
||||
format.scale = image.scale
|
||||
format.opaque = false
|
||||
format.preferredRange = .standard
|
||||
return UIGraphicsImageRenderer(size: image.size, format: format).image { _ in
|
||||
image.draw(in: CGRect(origin: .zero, size: image.size))
|
||||
}
|
||||
}
|
||||
|
||||
static func scrubPng(_ data: Data) throws -> Data {
|
||||
let data = Data(data)
|
||||
guard data.count >= pngSignature.count, data.prefix(pngSignature.count) == pngSignature else {
|
||||
throw MediaSanitizationError.invalidPng
|
||||
}
|
||||
|
||||
var output = pngSignature
|
||||
var offset = pngSignature.count
|
||||
while offset < data.count {
|
||||
guard data.count - offset >= 12 else {
|
||||
throw MediaSanitizationError.invalidPng
|
||||
}
|
||||
|
||||
let payloadLengthValue = try readUInt32BigEndian(data, at: offset)
|
||||
guard
|
||||
let payloadLength = Int(exactly: payloadLengthValue),
|
||||
payloadLength <= data.count - offset - 12
|
||||
else {
|
||||
throw MediaSanitizationError.invalidPng
|
||||
}
|
||||
let chunkLength = payloadLength + 12
|
||||
let typeStart = offset + 4
|
||||
let typeEnd = typeStart + 4
|
||||
let typeBytes = data[typeStart..<typeEnd]
|
||||
guard let type = String(bytes: typeBytes, encoding: .ascii) else {
|
||||
throw MediaSanitizationError.invalidPng
|
||||
}
|
||||
|
||||
let isAncillary = typeBytes[typeBytes.startIndex] & 0x20 != 0
|
||||
if !isAncillary || allowedPngAncillaryChunks.contains(type) {
|
||||
output.append(data[offset..<(offset + chunkLength)])
|
||||
}
|
||||
|
||||
offset += chunkLength
|
||||
if type == "IEND" {
|
||||
return output
|
||||
}
|
||||
}
|
||||
|
||||
throw MediaSanitizationError.invalidPng
|
||||
}
|
||||
|
||||
static func scrubJpeg(_ data: Data) throws -> Data {
|
||||
let data = Data(data)
|
||||
guard data.count >= 2, data[0] == 0xFF, data[1] == 0xD8 else {
|
||||
throw MediaSanitizationError.invalidJpeg
|
||||
}
|
||||
|
||||
var output = Data([0xFF, 0xD8])
|
||||
var offset = 2
|
||||
var inScan = false
|
||||
while offset < data.count {
|
||||
if inScan, data[offset] != 0xFF {
|
||||
let nextMarker = data[offset...].firstIndex(of: 0xFF) ?? data.endIndex
|
||||
output.append(data[offset..<nextMarker])
|
||||
offset = nextMarker
|
||||
continue
|
||||
}
|
||||
guard data[offset] == 0xFF else {
|
||||
throw MediaSanitizationError.invalidJpeg
|
||||
}
|
||||
|
||||
let markerStart = offset
|
||||
while offset < data.count, data[offset] == 0xFF {
|
||||
offset += 1
|
||||
}
|
||||
guard offset < data.count else {
|
||||
throw MediaSanitizationError.invalidJpeg
|
||||
}
|
||||
|
||||
let marker = data[offset]
|
||||
offset += 1
|
||||
if inScan, marker == 0x00 {
|
||||
output.append(data[markerStart..<offset])
|
||||
continue
|
||||
}
|
||||
if (0xD0...0xD7).contains(marker) || marker == 0x01 {
|
||||
output.append(data[markerStart..<offset])
|
||||
continue
|
||||
}
|
||||
if marker == 0xD9 {
|
||||
output.append(data[markerStart..<offset])
|
||||
return output
|
||||
}
|
||||
guard marker != 0xD8, data.count - offset >= 2 else {
|
||||
throw MediaSanitizationError.invalidJpeg
|
||||
}
|
||||
|
||||
let segmentLength = try readUInt16BigEndian(data, at: offset)
|
||||
guard segmentLength >= 2, Int(segmentLength) <= data.count - offset else {
|
||||
throw MediaSanitizationError.invalidJpeg
|
||||
}
|
||||
let segmentEnd = offset + Int(segmentLength)
|
||||
if shouldKeepJpegSegment(marker, data: data, payload: (offset + 2)..<segmentEnd) {
|
||||
output.append(data[markerStart..<segmentEnd])
|
||||
}
|
||||
|
||||
offset = segmentEnd
|
||||
inScan = marker == 0xDA
|
||||
}
|
||||
|
||||
throw MediaSanitizationError.invalidJpeg
|
||||
}
|
||||
|
||||
private static func shouldKeepJpegSegment(
|
||||
_ marker: UInt8,
|
||||
data: Data,
|
||||
payload: Range<Int>
|
||||
) -> Bool {
|
||||
switch marker {
|
||||
case 0xE0:
|
||||
guard
|
||||
payload.count >= 14,
|
||||
data[payload.lowerBound..<(payload.lowerBound + 5)].elementsEqual([
|
||||
0x4A, 0x46, 0x49, 0x46, 0x00,
|
||||
])
|
||||
else {
|
||||
return false
|
||||
}
|
||||
let thumbnailWidth = Int(data[payload.lowerBound + 12])
|
||||
let thumbnailHeight = Int(data[payload.lowerBound + 13])
|
||||
return payload.count == 14 + 3 * thumbnailWidth * thumbnailHeight
|
||||
case 0xEE:
|
||||
return payload.count == 12
|
||||
&& data[payload.lowerBound..<(payload.lowerBound + 5)].elementsEqual([
|
||||
0x41, 0x64, 0x6F, 0x62, 0x65,
|
||||
])
|
||||
case 0xE1...0xED, 0xEF, 0xFE:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private static func readUInt16BigEndian(_ data: Data, at offset: Int) throws -> UInt16 {
|
||||
guard data.count - offset >= 2 else {
|
||||
throw MediaSanitizationError.invalidJpeg
|
||||
}
|
||||
return UInt16(data[offset]) << 8 | UInt16(data[offset + 1])
|
||||
}
|
||||
|
||||
private static func readUInt32BigEndian(_ data: Data, at offset: Int) throws -> UInt32 {
|
||||
guard data.count - offset >= 4 else {
|
||||
throw MediaSanitizationError.invalidPng
|
||||
}
|
||||
return UInt32(data[offset]) << 24 | UInt32(data[offset + 1]) << 16
|
||||
| UInt32(data[offset + 2]) << 8 | UInt32(data[offset + 3])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
import CoreText
|
||||
import Flutter
|
||||
import UIKit
|
||||
|
||||
enum NativeAttachmentExpandedSurfaceBehavior {
|
||||
@MainActor
|
||||
static func dismissKeyboard(in window: UIWindow?) {
|
||||
window?.endEditing(true)
|
||||
}
|
||||
|
||||
static func keyboardOverlap(
|
||||
containerBounds: CGRect,
|
||||
keyboardLayoutFrame: CGRect
|
||||
) -> CGFloat {
|
||||
guard
|
||||
!keyboardLayoutFrame.isNull,
|
||||
!keyboardLayoutFrame.isInfinite,
|
||||
keyboardLayoutFrame.minY < containerBounds.maxY
|
||||
else {
|
||||
return 0
|
||||
}
|
||||
return max(0, containerBounds.maxY - keyboardLayoutFrame.minY)
|
||||
}
|
||||
}
|
||||
|
||||
enum NativeAttachmentPopoverAnchorLayout {
|
||||
static let expandedVerticalOffset: CGFloat = 40
|
||||
|
||||
static func sourceRect(
|
||||
anchorBounds: CGRect,
|
||||
keyboardDismissalOffset: CGFloat,
|
||||
isExpanded: Bool
|
||||
) -> CGRect {
|
||||
anchorBounds.offsetBy(
|
||||
dx: 0,
|
||||
dy: keyboardDismissalOffset
|
||||
+ (isExpanded ? expandedVerticalOffset : 0)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum NativeAttachmentPopoverPresentationLayout {
|
||||
static func keyboardDismissalOffset(
|
||||
sourceRect: CGRect,
|
||||
containerBounds: CGRect,
|
||||
safeAreaInsets: UIEdgeInsets,
|
||||
keyboardLayoutFrame: CGRect,
|
||||
menuHeight: CGFloat
|
||||
) -> CGFloat {
|
||||
let keyboardOverlap =
|
||||
NativeAttachmentExpandedSurfaceBehavior.keyboardOverlap(
|
||||
containerBounds: containerBounds,
|
||||
keyboardLayoutFrame: keyboardLayoutFrame
|
||||
)
|
||||
guard keyboardOverlap > 0 else { return 0 }
|
||||
|
||||
let availableHeight =
|
||||
sourceRect.minY - (containerBounds.minY + safeAreaInsets.top)
|
||||
return availableHeight >= menuHeight ? 0 : keyboardOverlap
|
||||
}
|
||||
|
||||
static func sourceRect(
|
||||
_ sourceRect: CGRect,
|
||||
keyboardDismissalOffset: CGFloat
|
||||
) -> CGRect {
|
||||
sourceRect.offsetBy(dx: 0, dy: keyboardDismissalOffset)
|
||||
}
|
||||
}
|
||||
|
||||
final class NativeAttachmentPopoverCoordinator: NSObject {
|
||||
private let channel: FlutterMethodChannel
|
||||
private weak var parentViewController: UIViewController?
|
||||
private weak var presentedController: UIViewController?
|
||||
private weak var sourceAnchorView: UIView?
|
||||
|
||||
init(
|
||||
messenger: FlutterBinaryMessenger,
|
||||
parentViewController: UIViewController?
|
||||
) {
|
||||
channel = FlutterMethodChannel(
|
||||
name: "buzz/native_attachment_popover",
|
||||
binaryMessenger: messenger
|
||||
)
|
||||
self.parentViewController = parentViewController
|
||||
super.init()
|
||||
|
||||
channel.setMethodCallHandler { [weak self] call, result in
|
||||
self?.handle(call, result: result)
|
||||
}
|
||||
}
|
||||
|
||||
private func handle(
|
||||
_ call: FlutterMethodCall,
|
||||
result: @escaping FlutterResult
|
||||
) {
|
||||
switch call.method {
|
||||
case "isSupported":
|
||||
if #available(iOS 26.0, *) {
|
||||
result(true)
|
||||
} else {
|
||||
result(false)
|
||||
}
|
||||
case "present":
|
||||
guard
|
||||
let arguments = call.arguments as? [String: Any],
|
||||
let x = arguments["x"] as? NSNumber,
|
||||
let y = arguments["y"] as? NSNumber,
|
||||
let width = arguments["width"] as? NSNumber,
|
||||
let height = arguments["height"] as? NSNumber
|
||||
else {
|
||||
result(
|
||||
FlutterError(
|
||||
code: "invalid_arguments",
|
||||
message: "Expected the attachment trigger bounds.",
|
||||
details: nil
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
let sourceRect = CGRect(
|
||||
x: CGFloat(truncating: x),
|
||||
y: CGFloat(truncating: y),
|
||||
width: CGFloat(truncating: width),
|
||||
height: CGFloat(truncating: height)
|
||||
)
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
result(self?.presentPopover(sourceRect: sourceRect) ?? false)
|
||||
}
|
||||
case "dismiss":
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
if #available(iOS 26.0, *),
|
||||
let controller =
|
||||
self?.presentedController
|
||||
as? NativeAttachmentPopoverViewController
|
||||
{
|
||||
controller.dismissAndNotify()
|
||||
} else {
|
||||
self?.presentedController?.dismiss(animated: true)
|
||||
}
|
||||
result(nil)
|
||||
}
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func presentPopover(sourceRect: CGRect) -> Bool {
|
||||
guard #available(iOS 26.0, *) else { return false }
|
||||
guard presentedController == nil else { return true }
|
||||
let rootViewController =
|
||||
parentViewController ?? activeWindowRootViewController()
|
||||
guard let presenter = topViewController(from: rootViewController) else {
|
||||
return false
|
||||
}
|
||||
|
||||
let sourceView = presenter.view
|
||||
var convertedRect: CGRect
|
||||
if let window = sourceView?.window {
|
||||
convertedRect = sourceView?.convert(sourceRect, from: window) ?? sourceRect
|
||||
} else {
|
||||
convertedRect = sourceRect
|
||||
}
|
||||
|
||||
if let sourceView {
|
||||
let keyboardDismissalOffset =
|
||||
NativeAttachmentPopoverPresentationLayout.keyboardDismissalOffset(
|
||||
sourceRect: convertedRect,
|
||||
containerBounds: sourceView.bounds,
|
||||
safeAreaInsets: sourceView.safeAreaInsets,
|
||||
keyboardLayoutFrame: sourceView.keyboardLayoutGuide.layoutFrame,
|
||||
menuHeight: NativeAttachmentMenuLayout.size(
|
||||
compatibleWith: sourceView.traitCollection
|
||||
).height
|
||||
)
|
||||
if keyboardDismissalOffset > 0 {
|
||||
NativeAttachmentExpandedSurfaceBehavior.dismissKeyboard(
|
||||
in: sourceView.window
|
||||
)
|
||||
convertedRect =
|
||||
NativeAttachmentPopoverPresentationLayout.sourceRect(
|
||||
convertedRect,
|
||||
keyboardDismissalOffset: keyboardDismissalOffset
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let anchorView = makeSourceAnchor(frame: convertedRect)
|
||||
sourceView?.addSubview(anchorView)
|
||||
sourceAnchorView = anchorView
|
||||
|
||||
let availableWidth = max(
|
||||
320,
|
||||
min(
|
||||
(sourceView?.bounds.width ?? UIScreen.main.bounds.width) - 24,
|
||||
430
|
||||
)
|
||||
)
|
||||
let controller = NativeAttachmentPopoverViewController(
|
||||
channel: channel,
|
||||
expandedWidth: availableWidth
|
||||
)
|
||||
controller.modalPresentationStyle = .popover
|
||||
controller.preferredTransition = .zoom { [weak anchorView] _ in
|
||||
anchorView
|
||||
}
|
||||
controller.onDismiss = { [weak self] in
|
||||
self?.presentedController = nil
|
||||
self?.sourceAnchorView?.removeFromSuperview()
|
||||
self?.channel.invokeMethod("dismissed", arguments: nil)
|
||||
}
|
||||
|
||||
guard let popover = controller.popoverPresentationController else {
|
||||
anchorView.removeFromSuperview()
|
||||
return false
|
||||
}
|
||||
popover.sourceView = anchorView
|
||||
popover.sourceRect = anchorView.bounds
|
||||
popover.permittedArrowDirections = [.down]
|
||||
popover.backgroundColor = .clear
|
||||
popover.delegate = controller
|
||||
|
||||
presentedController = controller
|
||||
presenter.present(controller, animated: true)
|
||||
return true
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func makeSourceAnchor(frame: CGRect) -> UIView {
|
||||
let anchor = UIView(frame: frame)
|
||||
anchor.isUserInteractionEnabled = false
|
||||
anchor.accessibilityElementsHidden = true
|
||||
anchor.backgroundColor = .clear
|
||||
anchor.layer.cornerRadius = min(frame.width, frame.height) / 2
|
||||
anchor.layer.cornerCurve = .continuous
|
||||
return anchor
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func activeWindowRootViewController() -> UIViewController? {
|
||||
UIApplication.shared.connectedScenes
|
||||
.compactMap { $0 as? UIWindowScene }
|
||||
.filter { $0.activationState == .foregroundActive }
|
||||
.flatMap(\.windows)
|
||||
.first(where: \.isKeyWindow)?
|
||||
.rootViewController
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func topViewController(
|
||||
from viewController: UIViewController?
|
||||
) -> UIViewController? {
|
||||
if let presented = viewController?.presentedViewController {
|
||||
return topViewController(from: presented)
|
||||
}
|
||||
if let navigation = viewController as? UINavigationController {
|
||||
return topViewController(from: navigation.visibleViewController)
|
||||
}
|
||||
if let tab = viewController as? UITabBarController {
|
||||
return topViewController(from: tab.selectedViewController)
|
||||
}
|
||||
return viewController
|
||||
}
|
||||
}
|
||||
|
||||
enum NativeAttachmentMenuLayout {
|
||||
static let itemCount: CGFloat = 4
|
||||
static let contentPadding: CGFloat = 16
|
||||
static let minimumItemHeight: CGFloat = 52
|
||||
static let itemSpacing: CGFloat = 8
|
||||
static let itemVerticalPadding: CGFloat = 8
|
||||
static let maximumHeight: CGFloat = 372
|
||||
static let width: CGFloat = 216
|
||||
static let labelTextStyle: UIFont.TextStyle = .title3
|
||||
|
||||
static func itemHeight(
|
||||
compatibleWith traitCollection: UITraitCollection
|
||||
) -> CGFloat {
|
||||
let labelHeight = NativeAttachmentMenuTypography.font(
|
||||
forTextStyle: labelTextStyle,
|
||||
compatibleWith: traitCollection
|
||||
).lineHeight
|
||||
return max(
|
||||
minimumItemHeight,
|
||||
ceil(labelHeight + (itemVerticalPadding * 2))
|
||||
)
|
||||
}
|
||||
|
||||
static func itemsHeight(
|
||||
compatibleWith traitCollection: UITraitCollection
|
||||
) -> CGFloat {
|
||||
(itemHeight(compatibleWith: traitCollection) * itemCount)
|
||||
+ (itemSpacing * (itemCount - 1))
|
||||
}
|
||||
|
||||
static func contentHeight(
|
||||
compatibleWith traitCollection: UITraitCollection
|
||||
) -> CGFloat {
|
||||
(contentPadding * 2) + itemsHeight(compatibleWith: traitCollection)
|
||||
}
|
||||
|
||||
static func size(
|
||||
compatibleWith traitCollection: UITraitCollection,
|
||||
maximumHeight: CGFloat = NativeAttachmentMenuLayout.maximumHeight
|
||||
) -> CGSize {
|
||||
CGSize(
|
||||
width: width,
|
||||
height: min(
|
||||
contentHeight(compatibleWith: traitCollection),
|
||||
maximumHeight
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum NativeAttachmentMenuTypography {
|
||||
static let interPostScriptName = "InterVariable"
|
||||
|
||||
private static let registeredInter: Bool = {
|
||||
let fontURL = Bundle.main.bundleURL
|
||||
.appendingPathComponent("Frameworks")
|
||||
.appendingPathComponent("App.framework")
|
||||
.appendingPathComponent("flutter_assets")
|
||||
.appendingPathComponent("assets")
|
||||
.appendingPathComponent("fonts")
|
||||
.appendingPathComponent("InterVariable.ttf")
|
||||
guard FileManager.default.fileExists(atPath: fontURL.path) else {
|
||||
return false
|
||||
}
|
||||
return CTFontManagerRegisterFontsForURL(
|
||||
fontURL as CFURL,
|
||||
.process,
|
||||
nil
|
||||
)
|
||||
}()
|
||||
|
||||
static func font(
|
||||
forTextStyle textStyle: UIFont.TextStyle,
|
||||
compatibleWith traitCollection: UITraitCollection? = nil
|
||||
) -> UIFont {
|
||||
_ = registeredInter
|
||||
let scaledPointSize = UIFontMetrics(forTextStyle: textStyle).scaledValue(
|
||||
for: 20,
|
||||
compatibleWith: traitCollection
|
||||
)
|
||||
let preferredFont = UIFont.preferredFont(
|
||||
forTextStyle: textStyle,
|
||||
compatibleWith: traitCollection
|
||||
)
|
||||
guard
|
||||
let interFont = UIFont(
|
||||
name: interPostScriptName,
|
||||
size: scaledPointSize
|
||||
)
|
||||
else {
|
||||
return preferredFont
|
||||
}
|
||||
return interFont
|
||||
}
|
||||
}
|
||||
|
||||
enum NativeAttachmentPopoverStyle {
|
||||
static let cornerRadius: CGFloat = 20
|
||||
static let shadowOpacity: Float = 0.18
|
||||
static let shadowRadius: CGFloat = 12
|
||||
static let shadowOffset = CGSize(width: 0, height: 6)
|
||||
static let borderWidth: CGFloat = 1
|
||||
}
|
||||
|
||||
func makeNativeAttachmentMenuButton(
|
||||
title: String,
|
||||
symbol: String,
|
||||
action: @escaping () -> Void
|
||||
) -> UIButton {
|
||||
let button = UIButton(
|
||||
primaryAction: UIAction { _ in
|
||||
UISelectionFeedbackGenerator().selectionChanged()
|
||||
action()
|
||||
}
|
||||
)
|
||||
button.accessibilityLabel = title
|
||||
|
||||
let symbolConfiguration = UIImage.SymbolConfiguration(
|
||||
pointSize: 18,
|
||||
weight: .regular
|
||||
)
|
||||
let iconView = UIImageView(
|
||||
image: UIImage(
|
||||
systemName: symbol,
|
||||
withConfiguration: symbolConfiguration
|
||||
)
|
||||
)
|
||||
iconView.tintColor = .label
|
||||
iconView.contentMode = .center
|
||||
iconView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = title
|
||||
titleLabel.textColor = .label
|
||||
titleLabel.font = NativeAttachmentMenuTypography.font(
|
||||
forTextStyle: NativeAttachmentMenuLayout.labelTextStyle
|
||||
)
|
||||
titleLabel.adjustsFontForContentSizeCategory = true
|
||||
titleLabel.textAlignment = .left
|
||||
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
button.addSubview(iconView)
|
||||
button.addSubview(titleLabel)
|
||||
NSLayoutConstraint.activate([
|
||||
iconView.leadingAnchor.constraint(
|
||||
equalTo: button.leadingAnchor,
|
||||
constant: 8
|
||||
),
|
||||
iconView.centerYAnchor.constraint(equalTo: button.centerYAnchor),
|
||||
iconView.widthAnchor.constraint(equalToConstant: 26),
|
||||
titleLabel.leadingAnchor.constraint(
|
||||
equalTo: iconView.trailingAnchor,
|
||||
constant: 12
|
||||
),
|
||||
titleLabel.trailingAnchor.constraint(
|
||||
equalTo: button.trailingAnchor,
|
||||
constant: -8
|
||||
),
|
||||
titleLabel.centerYAnchor.constraint(equalTo: button.centerYAnchor),
|
||||
])
|
||||
button.configurationUpdateHandler = { button in
|
||||
button.alpha = button.isHighlighted ? 0.62 : 1
|
||||
}
|
||||
return button
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
#import "GeneratedPluginRegistrant.h"
|
||||
@@ -0,0 +1,6 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
|
||||
class SceneDelegate: FlutterSceneDelegate {
|
||||
|
||||
}
|
||||
|
After Width: | Height: | Size: 896 B |
|
After Width: | Height: | Size: 312 B |
@@ -0,0 +1,632 @@
|
||||
import AVFoundation
|
||||
import Flutter
|
||||
import UIKit
|
||||
import XCTest
|
||||
|
||||
@testable import Buzz
|
||||
|
||||
class RunnerTests: XCTestCase {
|
||||
|
||||
func testRelativeTrackInsertionTimesPreserveAudioDelay() {
|
||||
let times = AppDelegate.relativeTrackInsertionTimes(
|
||||
videoStart: CMTime(seconds: 1, preferredTimescale: 600),
|
||||
audioStart: CMTime(seconds: 1.5, preferredTimescale: 600)
|
||||
)
|
||||
|
||||
XCTAssertEqual(CMTimeCompare(times.video, .zero), 0)
|
||||
XCTAssertEqual(
|
||||
CMTimeCompare(
|
||||
times.audio ?? .invalid,
|
||||
CMTime(seconds: 0.5, preferredTimescale: 600)
|
||||
),
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
func testRelativeTrackInsertionTimesPreserveVideoDelay() {
|
||||
let times = AppDelegate.relativeTrackInsertionTimes(
|
||||
videoStart: CMTime(seconds: 2, preferredTimescale: 600),
|
||||
audioStart: CMTime(seconds: 1, preferredTimescale: 600)
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
CMTimeCompare(
|
||||
times.video,
|
||||
CMTime(seconds: 1, preferredTimescale: 600)
|
||||
),
|
||||
0
|
||||
)
|
||||
XCTAssertEqual(CMTimeCompare(times.audio ?? .invalid, .zero), 0)
|
||||
}
|
||||
|
||||
func testRelativeTrackInsertionTimesZeroBasesVideoWithoutAudio() {
|
||||
let times = AppDelegate.relativeTrackInsertionTimes(
|
||||
videoStart: CMTime(seconds: 3, preferredTimescale: 600),
|
||||
audioStart: nil
|
||||
)
|
||||
|
||||
XCTAssertEqual(CMTimeCompare(times.video, .zero), 0)
|
||||
XCTAssertNil(times.audio)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testExpandedAttachmentSurfaceDismissesKeyboard() {
|
||||
let window = KeyboardDismissalSpyWindow()
|
||||
|
||||
NativeAttachmentExpandedSurfaceBehavior.dismissKeyboard(in: window)
|
||||
|
||||
XCTAssertTrue(window.didForceEndEditing)
|
||||
}
|
||||
|
||||
func testExpandedAttachmentSurfaceMeasuresKeyboardOverlap() {
|
||||
XCTAssertEqual(
|
||||
NativeAttachmentExpandedSurfaceBehavior.keyboardOverlap(
|
||||
containerBounds: CGRect(x: 0, y: 0, width: 390, height: 844),
|
||||
keyboardLayoutFrame: CGRect(
|
||||
x: 0,
|
||||
y: 544,
|
||||
width: 390,
|
||||
height: 300
|
||||
)
|
||||
),
|
||||
300
|
||||
)
|
||||
XCTAssertEqual(
|
||||
NativeAttachmentExpandedSurfaceBehavior.keyboardOverlap(
|
||||
containerBounds: CGRect(x: 0, y: 0, width: 390, height: 844),
|
||||
keyboardLayoutFrame: CGRect(x: 0, y: 844, width: 390, height: 0)
|
||||
),
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
func testAttachmentMenuReturnsToKeyboardDismissedAnchor() {
|
||||
let anchorBounds = CGRect(x: 0, y: 0, width: 44, height: 44)
|
||||
|
||||
XCTAssertEqual(
|
||||
NativeAttachmentPopoverAnchorLayout.sourceRect(
|
||||
anchorBounds: anchorBounds,
|
||||
keyboardDismissalOffset: 300,
|
||||
isExpanded: true
|
||||
),
|
||||
anchorBounds.offsetBy(dx: 0, dy: 340)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
NativeAttachmentPopoverAnchorLayout.sourceRect(
|
||||
anchorBounds: anchorBounds,
|
||||
keyboardDismissalOffset: 300,
|
||||
isExpanded: false
|
||||
),
|
||||
anchorBounds.offsetBy(dx: 0, dy: 300)
|
||||
)
|
||||
}
|
||||
|
||||
func testAttachmentMenuKeepsKeyboardWhenMenuFitsAboveTrigger() {
|
||||
XCTAssertEqual(
|
||||
NativeAttachmentPopoverPresentationLayout.keyboardDismissalOffset(
|
||||
sourceRect: CGRect(x: 320, y: 480, width: 44, height: 44),
|
||||
containerBounds: CGRect(x: 0, y: 0, width: 390, height: 844),
|
||||
safeAreaInsets: UIEdgeInsets(top: 59, left: 0, bottom: 34, right: 0),
|
||||
keyboardLayoutFrame: CGRect(
|
||||
x: 0,
|
||||
y: 544,
|
||||
width: 390,
|
||||
height: 300
|
||||
),
|
||||
menuHeight: NativeAttachmentMenuLayout.size(
|
||||
compatibleWith: UITraitCollection(
|
||||
preferredContentSizeCategory: .large
|
||||
)
|
||||
).height
|
||||
),
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
func testAttachmentMenuDismissesKeyboardAndRepositionsInCompactHeight() {
|
||||
let sourceRect = CGRect(x: 760, y: 168, width: 44, height: 44)
|
||||
let keyboardDismissalOffset =
|
||||
NativeAttachmentPopoverPresentationLayout.keyboardDismissalOffset(
|
||||
sourceRect: sourceRect,
|
||||
containerBounds: CGRect(x: 0, y: 0, width: 844, height: 390),
|
||||
safeAreaInsets: UIEdgeInsets(top: 0, left: 59, bottom: 21, right: 59),
|
||||
keyboardLayoutFrame: CGRect(
|
||||
x: 0,
|
||||
y: 228,
|
||||
width: 844,
|
||||
height: 162
|
||||
),
|
||||
menuHeight: NativeAttachmentMenuLayout.size(
|
||||
compatibleWith: UITraitCollection(
|
||||
preferredContentSizeCategory: .large
|
||||
)
|
||||
).height
|
||||
)
|
||||
|
||||
XCTAssertEqual(keyboardDismissalOffset, 162)
|
||||
XCTAssertEqual(
|
||||
NativeAttachmentPopoverPresentationLayout.sourceRect(
|
||||
sourceRect,
|
||||
keyboardDismissalOffset: keyboardDismissalOffset
|
||||
),
|
||||
sourceRect.offsetBy(dx: 0, dy: 162)
|
||||
)
|
||||
}
|
||||
|
||||
func testAttachmentMenuDoesNotMoveWithoutSoftwareKeyboard() {
|
||||
XCTAssertEqual(
|
||||
NativeAttachmentPopoverPresentationLayout.keyboardDismissalOffset(
|
||||
sourceRect: CGRect(x: 760, y: 168, width: 44, height: 44),
|
||||
containerBounds: CGRect(x: 0, y: 0, width: 844, height: 390),
|
||||
safeAreaInsets: UIEdgeInsets(top: 0, left: 59, bottom: 21, right: 59),
|
||||
keyboardLayoutFrame: CGRect(x: 0, y: 390, width: 844, height: 0),
|
||||
menuHeight: NativeAttachmentMenuLayout.size(
|
||||
compatibleWith: UITraitCollection(
|
||||
preferredContentSizeCategory: .large
|
||||
)
|
||||
).height
|
||||
),
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
func testNativeAttachmentMenuUsesRoomyRowsAndInsets() {
|
||||
let traits = UITraitCollection(preferredContentSizeCategory: .large)
|
||||
let size = NativeAttachmentMenuLayout.size(compatibleWith: traits)
|
||||
|
||||
XCTAssertEqual(size.width, 216)
|
||||
XCTAssertEqual(size.height, 264)
|
||||
XCTAssertEqual(NativeAttachmentMenuLayout.contentPadding, 16)
|
||||
XCTAssertEqual(
|
||||
NativeAttachmentMenuLayout.itemHeight(compatibleWith: traits),
|
||||
52
|
||||
)
|
||||
XCTAssertEqual(NativeAttachmentMenuLayout.itemSpacing, 8)
|
||||
XCTAssertEqual(NativeAttachmentMenuLayout.labelTextStyle, .title3)
|
||||
}
|
||||
|
||||
func testNativeAttachmentMenuUsesInterAndSharedPopoverChrome() {
|
||||
let font = NativeAttachmentMenuTypography.font(
|
||||
forTextStyle: NativeAttachmentMenuLayout.labelTextStyle
|
||||
)
|
||||
var didSelect = false
|
||||
let button = makeNativeAttachmentMenuButton(
|
||||
title: "Photos",
|
||||
symbol: "photo",
|
||||
action: { didSelect = true }
|
||||
)
|
||||
let titleLabel = button.subviews.compactMap { $0 as? UILabel }.first
|
||||
|
||||
XCTAssertTrue(font.fontName.hasPrefix("Inter"))
|
||||
XCTAssertTrue(titleLabel?.font.fontName.hasPrefix("Inter") == true)
|
||||
XCTAssertEqual(NativeAttachmentPopoverStyle.cornerRadius, 20)
|
||||
XCTAssertEqual(NativeAttachmentPopoverStyle.borderWidth, 1)
|
||||
XCTAssertEqual(NativeAttachmentPopoverStyle.shadowOpacity, 0.18)
|
||||
|
||||
button.sendActions(for: .primaryActionTriggered)
|
||||
XCTAssertTrue(didSelect)
|
||||
}
|
||||
|
||||
func testNativeAttachmentMenuGrowsAndScrollsForAccessibilityText() {
|
||||
let traits = UITraitCollection(
|
||||
preferredContentSizeCategory: .accessibilityExtraExtraExtraLarge
|
||||
)
|
||||
let itemHeight = NativeAttachmentMenuLayout.itemHeight(
|
||||
compatibleWith: traits
|
||||
)
|
||||
let contentHeight = NativeAttachmentMenuLayout.contentHeight(
|
||||
compatibleWith: traits
|
||||
)
|
||||
let size = NativeAttachmentMenuLayout.size(compatibleWith: traits)
|
||||
|
||||
XCTAssertGreaterThan(itemHeight, 52)
|
||||
XCTAssertGreaterThan(contentHeight, 264)
|
||||
XCTAssertEqual(
|
||||
size.height,
|
||||
min(contentHeight, NativeAttachmentMenuLayout.maximumHeight)
|
||||
)
|
||||
XCTAssertLessThanOrEqual(
|
||||
size.height,
|
||||
NativeAttachmentMenuLayout.maximumHeight
|
||||
)
|
||||
}
|
||||
|
||||
func testDynamicIslandQrScannerRecognizesTallSafeAreas() {
|
||||
for safeAreaTopInset in [51, 59, 62] {
|
||||
XCTAssertTrue(
|
||||
AppDelegate.usesDynamicIslandQrScannerPortal(
|
||||
safeAreaTopInset: CGFloat(safeAreaTopInset)
|
||||
),
|
||||
"\(safeAreaTopInset)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func testDynamicIslandQrScannerRejectsStandardSafeAreas() {
|
||||
for safeAreaTopInset in [0, 44, 47, 50] {
|
||||
XCTAssertFalse(
|
||||
AppDelegate.usesDynamicIslandQrScannerPortal(
|
||||
safeAreaTopInset: CGFloat(safeAreaTopInset)
|
||||
),
|
||||
"\(safeAreaTopInset)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func testClipboardImageDataPrefersOriginalPngBytes() throws {
|
||||
let pasteboard = try XCTUnwrap(
|
||||
UIPasteboard(name: UIPasteboard.Name(UUID().uuidString), create: true)
|
||||
)
|
||||
defer { UIPasteboard.remove(withName: pasteboard.name) }
|
||||
let pngData = Data([0x89, 0x50, 0x4E, 0x47])
|
||||
let jpegData = Data([0xFF, 0xD8, 0xFF])
|
||||
pasteboard.setItems([
|
||||
["public.png": pngData, "public.jpeg": jpegData]
|
||||
])
|
||||
|
||||
XCTAssertEqual(AppDelegate.clipboardImageData(from: pasteboard), pngData)
|
||||
}
|
||||
|
||||
func testClipboardImageDataPreservesOriginalWebPBytesForValidation() throws {
|
||||
let pasteboard = try XCTUnwrap(
|
||||
UIPasteboard(name: UIPasteboard.Name(UUID().uuidString), create: true)
|
||||
)
|
||||
defer { UIPasteboard.remove(withName: pasteboard.name) }
|
||||
let webPData = Data("RIFFxxxxWEBP".utf8)
|
||||
pasteboard.setData(webPData, forPasteboardType: "org.webmproject.webp")
|
||||
|
||||
XCTAssertEqual(AppDelegate.clipboardImageData(from: pasteboard), webPData)
|
||||
}
|
||||
|
||||
func testClipboardImageDataPreservesOriginalGifBytesForValidation() throws {
|
||||
let pasteboard = try XCTUnwrap(
|
||||
UIPasteboard(name: UIPasteboard.Name(UUID().uuidString), create: true)
|
||||
)
|
||||
defer { UIPasteboard.remove(withName: pasteboard.name) }
|
||||
let gifData = Data("GIF89a".utf8)
|
||||
pasteboard.setData(gifData, forPasteboardType: "com.compuserve.gif")
|
||||
|
||||
XCTAssertEqual(AppDelegate.clipboardImageData(from: pasteboard), gifData)
|
||||
}
|
||||
|
||||
func testClipboardImageDataReturnsNilWithoutAnImage() throws {
|
||||
let pasteboard = try XCTUnwrap(
|
||||
UIPasteboard(name: UIPasteboard.Name(UUID().uuidString), create: true)
|
||||
)
|
||||
defer { UIPasteboard.remove(withName: pasteboard.name) }
|
||||
pasteboard.string = "text only"
|
||||
|
||||
XCTAssertNil(AppDelegate.clipboardImageData(from: pasteboard))
|
||||
}
|
||||
|
||||
func testSanitizePngRemovesUIKitMetadataChunks() throws {
|
||||
let fixture = try fixtureData(named: "UIKitEncoded", extension: "png")
|
||||
XCTAssertEqual(
|
||||
try pngChunkTypes(fixture),
|
||||
[
|
||||
"IHDR", "sRGB", "eXIf", "pHYs", "iDOT", "IDAT", "IDAT", "IEND",
|
||||
])
|
||||
|
||||
let sanitized = try MediaSanitizer.scrubPng(fixture)
|
||||
|
||||
XCTAssertEqual(
|
||||
try pngChunkTypes(sanitized),
|
||||
[
|
||||
"IHDR", "sRGB", "IDAT", "IDAT", "IEND",
|
||||
])
|
||||
try assertMatchesRelayImageMetadataPolicy(sanitized, mimeType: "image/png")
|
||||
XCTAssertNotNil(UIImage(data: sanitized))
|
||||
|
||||
var withTrailingPayload = fixture
|
||||
withTrailingPayload.append(Data("hidden location".utf8))
|
||||
let scrubbedTrailingPayload = try MediaSanitizer.scrubPng(withTrailingPayload)
|
||||
XCTAssertEqual(scrubbedTrailingPayload, sanitized)
|
||||
}
|
||||
|
||||
func testSanitizePngSupportsDataSlices() throws {
|
||||
let fixture = try fixtureData(named: "UIKitEncoded", extension: "png")
|
||||
let padded = Data([0x00]) + fixture
|
||||
let slice = padded.dropFirst()
|
||||
XCTAssertNotEqual(slice.startIndex, 0)
|
||||
|
||||
let sanitized = try MediaSanitizer.scrubPng(slice)
|
||||
|
||||
try assertMatchesRelayImageMetadataPolicy(sanitized, mimeType: "image/png")
|
||||
XCTAssertNotNil(UIImage(data: sanitized))
|
||||
}
|
||||
|
||||
func testSanitizeJpegRemovesUIKitMetadataSegments() throws {
|
||||
let fixture = try fixtureData(named: "UIKitEncoded", extension: "jpg")
|
||||
XCTAssertEqual(try jpegMetadataMarkers(fixture), [0xE0, 0xE1, 0xED])
|
||||
|
||||
let sanitized = try MediaSanitizer.scrubJpeg(fixture)
|
||||
|
||||
XCTAssertEqual(try jpegMetadataMarkers(sanitized), [0xE0])
|
||||
try assertMatchesRelayImageMetadataPolicy(sanitized, mimeType: "image/jpeg")
|
||||
XCTAssertNotNil(UIImage(data: sanitized))
|
||||
|
||||
var withTrailingPayload = fixture
|
||||
withTrailingPayload.append(Data("hidden location".utf8))
|
||||
let scrubbedTrailingPayload = try MediaSanitizer.scrubJpeg(withTrailingPayload)
|
||||
XCTAssertEqual(scrubbedTrailingPayload, sanitized)
|
||||
}
|
||||
|
||||
func testSanitizeJpegSupportsDataSlices() throws {
|
||||
let fixture = try fixtureData(named: "UIKitEncoded", extension: "jpg")
|
||||
let padded = Data([0x00]) + fixture
|
||||
let slice = padded.dropFirst()
|
||||
XCTAssertNotEqual(slice.startIndex, 0)
|
||||
|
||||
let sanitized = try MediaSanitizer.scrubJpeg(slice)
|
||||
|
||||
try assertMatchesRelayImageMetadataPolicy(sanitized, mimeType: "image/jpeg")
|
||||
XCTAssertNotNil(UIImage(data: sanitized))
|
||||
}
|
||||
|
||||
func testEncodeJpegScrubsUIKitOutput() throws {
|
||||
let fixture = try fixtureData(named: "UIKitEncoded", extension: "jpg")
|
||||
let image = try XCTUnwrap(UIImage(data: fixture))
|
||||
|
||||
let encoded = try XCTUnwrap(MediaSanitizer.encodeJpeg(image))
|
||||
|
||||
try assertMatchesRelayImageMetadataPolicy(encoded, mimeType: "image/jpeg")
|
||||
XCTAssertNotNil(UIImage(data: encoded))
|
||||
}
|
||||
|
||||
func testSanitizeDisplayP3ImagePreservesRenderedColorInSRGB() throws {
|
||||
let image = try displayP3Image(red: 0.9, green: 0.2, blue: 0.1)
|
||||
let expectedColor = try sRGBPixel(from: image)
|
||||
let mimeTypesAndAccuracy: [(mimeType: String, accuracy: UInt8)] = [
|
||||
("image/png", 0), ("image/jpeg", 1),
|
||||
]
|
||||
|
||||
for (mimeType, accuracy) in mimeTypesAndAccuracy {
|
||||
let sanitized = try XCTUnwrap(
|
||||
MediaSanitizer.sanitizeImage(image, mimeType: mimeType),
|
||||
"Failed to sanitize Display-P3 image as \(mimeType)"
|
||||
)
|
||||
|
||||
try assertMatchesRelayImageMetadataPolicy(sanitized, mimeType: mimeType)
|
||||
let decoded = try XCTUnwrap(UIImage(data: sanitized))
|
||||
XCTAssertEqual(
|
||||
decoded.cgImage?.colorSpace?.name,
|
||||
CGColorSpace(name: CGColorSpace.sRGB)?.name
|
||||
)
|
||||
let actualColor = try sRGBPixel(from: decoded)
|
||||
XCTAssertEqual(actualColor.count, expectedColor.count)
|
||||
for (actual, expected) in zip(actualColor, expectedColor) {
|
||||
XCTAssertLessThanOrEqual(
|
||||
actual > expected ? actual - expected : expected - actual,
|
||||
accuracy
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func displayP3Image(red: CGFloat, green: CGFloat, blue: CGFloat) throws -> UIImage {
|
||||
let colorSpace = try XCTUnwrap(CGColorSpace(name: CGColorSpace.displayP3))
|
||||
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
|
||||
let context = try XCTUnwrap(
|
||||
CGContext(
|
||||
data: nil,
|
||||
width: 1,
|
||||
height: 1,
|
||||
bitsPerComponent: 8,
|
||||
bytesPerRow: 4,
|
||||
space: colorSpace,
|
||||
bitmapInfo: bitmapInfo.rawValue
|
||||
)
|
||||
)
|
||||
context.setFillColor(
|
||||
try XCTUnwrap(CGColor(colorSpace: colorSpace, components: [red, green, blue, 1]))
|
||||
)
|
||||
context.fill(CGRect(x: 0, y: 0, width: 1, height: 1))
|
||||
return UIImage(cgImage: try XCTUnwrap(context.makeImage()))
|
||||
}
|
||||
|
||||
private func sRGBPixel(from image: UIImage) throws -> [UInt8] {
|
||||
let colorSpace = try XCTUnwrap(CGColorSpace(name: CGColorSpace.sRGB))
|
||||
var bytes = [UInt8](repeating: 0, count: 4)
|
||||
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
|
||||
let context = try bytes.withUnsafeMutableBytes { bytes in
|
||||
try XCTUnwrap(
|
||||
CGContext(
|
||||
data: bytes.baseAddress,
|
||||
width: 1,
|
||||
height: 1,
|
||||
bitsPerComponent: 8,
|
||||
bytesPerRow: 4,
|
||||
space: colorSpace,
|
||||
bitmapInfo: bitmapInfo.rawValue
|
||||
)
|
||||
)
|
||||
}
|
||||
context.interpolationQuality = .none
|
||||
context.draw(try XCTUnwrap(image.cgImage), in: CGRect(x: 0, y: 0, width: 1, height: 1))
|
||||
return bytes
|
||||
}
|
||||
|
||||
private func fixtureData(named name: String, extension fileExtension: String) throws -> Data {
|
||||
let url = try XCTUnwrap(
|
||||
Bundle(for: RunnerTests.self).url(forResource: name, withExtension: fileExtension))
|
||||
return try Data(contentsOf: url)
|
||||
}
|
||||
}
|
||||
|
||||
private final class KeyboardDismissalSpyWindow: UIWindow {
|
||||
private(set) var didForceEndEditing = false
|
||||
|
||||
override func endEditing(_ force: Bool) -> Bool {
|
||||
didForceEndEditing = force
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private enum RelayImagePolicyError: Error {
|
||||
case invalidPng
|
||||
case invalidJpeg
|
||||
case metadataForbidden
|
||||
}
|
||||
|
||||
private let pngSignature = Data([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])
|
||||
private let allowedPngAncillaryChunks: Set<String> = [
|
||||
"cHRM", "gAMA", "sBIT", "sRGB", "bKGD", "hIST", "tRNS", "sPLT", "acTL", "fcTL", "fdAT",
|
||||
]
|
||||
|
||||
private func assertMatchesRelayImageMetadataPolicy(_ data: Data, mimeType: String) throws {
|
||||
switch mimeType {
|
||||
case "image/png":
|
||||
guard data.count >= pngSignature.count, data.prefix(pngSignature.count) == pngSignature else {
|
||||
throw RelayImagePolicyError.invalidPng
|
||||
}
|
||||
var offset = pngSignature.count
|
||||
while offset < data.count {
|
||||
guard data.count - offset >= 12 else { throw RelayImagePolicyError.invalidPng }
|
||||
let payloadLength = Int(try readUInt32BigEndian(data, at: offset))
|
||||
guard payloadLength <= data.count - offset - 12 else {
|
||||
throw RelayImagePolicyError.invalidPng
|
||||
}
|
||||
let typeBytes = data[(offset + 4)..<(offset + 8)]
|
||||
guard let type = String(bytes: typeBytes, encoding: .ascii) else {
|
||||
throw RelayImagePolicyError.invalidPng
|
||||
}
|
||||
let chunkEnd = offset + payloadLength + 12
|
||||
let isAncillary = typeBytes[typeBytes.startIndex] & 0x20 != 0
|
||||
if isAncillary, !allowedPngAncillaryChunks.contains(type) {
|
||||
throw RelayImagePolicyError.metadataForbidden
|
||||
}
|
||||
offset = chunkEnd
|
||||
if type == "IEND" {
|
||||
guard offset == data.count else { throw RelayImagePolicyError.metadataForbidden }
|
||||
return
|
||||
}
|
||||
}
|
||||
throw RelayImagePolicyError.invalidPng
|
||||
case "image/jpeg":
|
||||
guard data.count >= 2, data[0] == 0xFF, data[1] == 0xD8 else {
|
||||
throw RelayImagePolicyError.invalidJpeg
|
||||
}
|
||||
var offset = 2
|
||||
var inScan = false
|
||||
while offset < data.count {
|
||||
if inScan, data[offset] != 0xFF {
|
||||
offset += 1
|
||||
continue
|
||||
}
|
||||
guard data[offset] == 0xFF else { throw RelayImagePolicyError.invalidJpeg }
|
||||
while offset < data.count, data[offset] == 0xFF { offset += 1 }
|
||||
guard offset < data.count else { throw RelayImagePolicyError.invalidJpeg }
|
||||
let marker = data[offset]
|
||||
offset += 1
|
||||
if inScan, marker == 0x00 { continue }
|
||||
if (0xD0...0xD7).contains(marker) || marker == 0x01 { continue }
|
||||
if marker == 0xD9 {
|
||||
guard offset == data.count else { throw RelayImagePolicyError.metadataForbidden }
|
||||
return
|
||||
}
|
||||
guard marker != 0xD8, data.count - offset >= 2 else {
|
||||
throw RelayImagePolicyError.invalidJpeg
|
||||
}
|
||||
let length = Int(try readUInt16BigEndian(data, at: offset))
|
||||
guard length >= 2, length <= data.count - offset else {
|
||||
throw RelayImagePolicyError.invalidJpeg
|
||||
}
|
||||
let payload = (offset + 2)..<(offset + length)
|
||||
if marker == 0xE0 {
|
||||
guard
|
||||
payload.count >= 14,
|
||||
data[payload.lowerBound..<(payload.lowerBound + 5)].elementsEqual([
|
||||
0x4A, 0x46, 0x49, 0x46, 0x00,
|
||||
]),
|
||||
payload.count
|
||||
== 14 + 3 * Int(data[payload.lowerBound + 12]) * Int(data[payload.lowerBound + 13])
|
||||
else {
|
||||
throw RelayImagePolicyError.metadataForbidden
|
||||
}
|
||||
} else if marker == 0xEE {
|
||||
guard
|
||||
payload.count == 12,
|
||||
data[payload.lowerBound..<(payload.lowerBound + 5)].elementsEqual([
|
||||
0x41, 0x64, 0x6F, 0x62, 0x65,
|
||||
])
|
||||
else {
|
||||
throw RelayImagePolicyError.metadataForbidden
|
||||
}
|
||||
} else if (0xE1...0xED).contains(marker) || marker == 0xEF || marker == 0xFE {
|
||||
throw RelayImagePolicyError.metadataForbidden
|
||||
}
|
||||
offset += length
|
||||
inScan = marker == 0xDA
|
||||
}
|
||||
throw RelayImagePolicyError.invalidJpeg
|
||||
default:
|
||||
XCTFail("Unsupported test MIME type: \(mimeType)")
|
||||
}
|
||||
}
|
||||
|
||||
private func pngChunkTypes(_ data: Data) throws -> [String] {
|
||||
guard data.count >= pngSignature.count, data.prefix(pngSignature.count) == pngSignature else {
|
||||
throw RelayImagePolicyError.invalidPng
|
||||
}
|
||||
var result: [String] = []
|
||||
var offset = pngSignature.count
|
||||
while offset < data.count {
|
||||
guard data.count - offset >= 12 else { throw RelayImagePolicyError.invalidPng }
|
||||
let payloadLength = Int(try readUInt32BigEndian(data, at: offset))
|
||||
guard payloadLength <= data.count - offset - 12 else { throw RelayImagePolicyError.invalidPng }
|
||||
guard let type = String(bytes: data[(offset + 4)..<(offset + 8)], encoding: .ascii) else {
|
||||
throw RelayImagePolicyError.invalidPng
|
||||
}
|
||||
result.append(type)
|
||||
offset += payloadLength + 12
|
||||
if type == "IEND" { return result }
|
||||
}
|
||||
throw RelayImagePolicyError.invalidPng
|
||||
}
|
||||
|
||||
private func jpegMetadataMarkers(_ data: Data) throws -> [UInt8] {
|
||||
guard data.count >= 2, data[0] == 0xFF, data[1] == 0xD8 else {
|
||||
throw RelayImagePolicyError.invalidJpeg
|
||||
}
|
||||
var result: [UInt8] = []
|
||||
var offset = 2
|
||||
var inScan = false
|
||||
while offset < data.count {
|
||||
if inScan, data[offset] != 0xFF {
|
||||
offset += 1
|
||||
continue
|
||||
}
|
||||
guard data[offset] == 0xFF else { throw RelayImagePolicyError.invalidJpeg }
|
||||
while offset < data.count, data[offset] == 0xFF { offset += 1 }
|
||||
guard offset < data.count else { throw RelayImagePolicyError.invalidJpeg }
|
||||
let marker = data[offset]
|
||||
offset += 1
|
||||
if inScan, marker == 0x00 { continue }
|
||||
if (0xD0...0xD7).contains(marker) || marker == 0x01 { continue }
|
||||
if marker == 0xD9 { return result }
|
||||
guard marker != 0xD8, data.count - offset >= 2 else {
|
||||
throw RelayImagePolicyError.invalidJpeg
|
||||
}
|
||||
let length = Int(try readUInt16BigEndian(data, at: offset))
|
||||
guard length >= 2, length <= data.count - offset else {
|
||||
throw RelayImagePolicyError.invalidJpeg
|
||||
}
|
||||
if (0xE0...0xEF).contains(marker) || marker == 0xFE {
|
||||
result.append(marker)
|
||||
}
|
||||
offset += length
|
||||
inScan = marker == 0xDA
|
||||
}
|
||||
throw RelayImagePolicyError.invalidJpeg
|
||||
}
|
||||
|
||||
private func readUInt16BigEndian(_ data: Data, at offset: Int) throws -> UInt16 {
|
||||
guard data.count - offset >= 2 else { throw RelayImagePolicyError.invalidJpeg }
|
||||
return UInt16(data[offset]) << 8 | UInt16(data[offset + 1])
|
||||
}
|
||||
|
||||
private func readUInt32BigEndian(_ data: Data, at offset: Int) throws -> UInt32 {
|
||||
guard data.count - offset >= 4 else { throw RelayImagePolicyError.invalidPng }
|
||||
return UInt32(data[offset]) << 24 | UInt32(data[offset + 1]) << 16
|
||||
| UInt32(data[offset + 2]) << 8 | UInt32(data[offset + 3])
|
||||
}
|
||||