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,4 @@
|
||||
{
|
||||
"BUZZ_RELAY_URL": "http://localhost:3000",
|
||||
"BUZZ_DEV_PUBKEY": "<your-hex-pubkey-here>"
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
/coverage/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
# Local environment config (contains keys)
|
||||
.env.json
|
||||
@@ -0,0 +1,33 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "db50e20168db8fee486b9abf32fc912de3bc5b6a"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
|
||||
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
|
||||
- platform: android
|
||||
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
|
||||
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
|
||||
- platform: ios
|
||||
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
|
||||
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
@@ -0,0 +1,80 @@
|
||||
# Changelog
|
||||
|
||||
## mobile-v0.4.11
|
||||
|
||||
- fix(mobile): stop media fetch stampede — stable cache keys, bounded decode, 429 cooldown ([#2219](https://github.com/block/buzz/pull/2219)) ([`591bbbfa7`](https://github.com/block/buzz/commit/591bbbfa7e5931318a226555d6bf921ba50d97c2))
|
||||
|
||||
|
||||
## mobile-v0.4.9
|
||||
|
||||
- fix(mobile): sanitize Android image uploads ([#2188](https://github.com/block/buzz/pull/2188)) ([`ee21da90b`](https://github.com/block/buzz/commit/ee21da90bd6b1da6bfaaf22ba00749398aaa9640))
|
||||
- chore(release): release Buzz Mobile version 0.4.8 ([#2187](https://github.com/block/buzz/pull/2187)) ([`fb8c90cf5`](https://github.com/block/buzz/commit/fb8c90cf597960e48fe55a9baceb8a5a08112ccc))
|
||||
- fix(mobile): image upload fails due to unstripped metadata ([#2185](https://github.com/block/buzz/pull/2185)) ([`37f15b200`](https://github.com/block/buzz/commit/37f15b20019169363b697aee41c99573b7bc3f24))
|
||||
- chore(deps): update all non-major dependencies ([#2152](https://github.com/block/buzz/pull/2152)) ([`6da407742`](https://github.com/block/buzz/commit/6da407742a09ee531c53a99ca5d2d314f0b38aef))
|
||||
|
||||
|
||||
## mobile-v0.4.8
|
||||
|
||||
- fix(mobile): image upload fails due to unstripped metadata ([#2185](https://github.com/block/buzz/pull/2185)) ([`37f15b200`](https://github.com/block/buzz/commit/37f15b20019169363b697aee41c99573b7bc3f24))
|
||||
- chore(deps): update all non-major dependencies ([#2152](https://github.com/block/buzz/pull/2152)) ([`6da407742`](https://github.com/block/buzz/commit/6da407742a09ee531c53a99ca5d2d314f0b38aef))
|
||||
|
||||
|
||||
## mobile-v0.4.7
|
||||
|
||||
- fix(ui): relabel agent owner attribution from "owned by" to "managed by" ([#2133](https://github.com/block/buzz/pull/2133)) ([`a1e977cd1`](https://github.com/block/buzz/commit/a1e977cd1bda3f5421533b47f12c38f4a336224e))
|
||||
- chore(release): release Buzz Mobile version 0.4.6-rc.1 ([#2049](https://github.com/block/buzz/pull/2049)) ([`3aba3a531`](https://github.com/block/buzz/commit/3aba3a5316ae8bb1aed518e01e68c3e3a2166a46))
|
||||
- Strip media metadata on clients and reject it at the relay ([#2006](https://github.com/block/buzz/pull/2006)) ([`5cfd69cb0`](https://github.com/block/buzz/commit/5cfd69cb0cf1dc63d718454defe3b8a8aaf5f15b))
|
||||
|
||||
|
||||
## mobile-v0.4.6-rc.1
|
||||
|
||||
- Strip media metadata on clients and reject it at the relay ([#2006](https://github.com/block/buzz/pull/2006)) ([`5cfd69cb`](https://github.com/block/buzz/commit/5cfd69cb0cf1dc63d718454defe3b8a8aaf5f15b))
|
||||
|
||||
|
||||
## mobile-v0.4.5
|
||||
|
||||
- fix(mobile): support open pairing relays ([#1939](https://github.com/block/buzz/pull/1939)) ([`a0081944`](https://github.com/block/buzz/commit/a0081944edbb3d33a2bcee0b4890ae5ef8ad4966))
|
||||
- fix(mobile): replace pairing placeholder with Buzz icon ([#1952](https://github.com/block/buzz/pull/1952)) ([`7eea924f`](https://github.com/block/buzz/commit/7eea924f3d7cf9ee816ae5f206c88762274fd7c6))
|
||||
- feat(media): require auth for relay media reads ([#1926](https://github.com/block/buzz/pull/1926)) ([`f3087628`](https://github.com/block/buzz/commit/f3087628524951de91028c9d263bcd0d0a727fab))
|
||||
|
||||
|
||||
## mobile-v0.4.4
|
||||
|
||||
- fix(mobile): surface non-member people and owned agents in @mention autocomplete ([#1877](https://github.com/block/buzz/pull/1877)) ([`54f41659`](https://github.com/block/buzz/commit/54f4165942ab5b3e2cf1fa1b47bbb907587cd5f4))
|
||||
- fix(mobile): open profile sheet when tapping @mentions ([#1876](https://github.com/block/buzz/pull/1876)) ([`7e009a89`](https://github.com/block/buzz/commit/7e009a893748eb660efd6d51d78f172cc913e3da))
|
||||
- fix(mobile): handle buzz:// links ([#1826](https://github.com/block/buzz/pull/1826)) ([`37f65c08`](https://github.com/block/buzz/commit/37f65c08dc4fc357e54283b19d6019b18cd2b00e))
|
||||
|
||||
|
||||
## mobile-v0.4.3
|
||||
|
||||
- fix(mobile): fix agent avatars on mobile ([#1837](https://github.com/block/buzz/pull/1837)) ([`7b6e1265`](https://github.com/block/buzz/commit/7b6e12657d69316614726065db69cc418750a973))
|
||||
- refactor(clients): standardize product naming on community ([#1858](https://github.com/block/buzz/pull/1858)) ([`3e76481a`](https://github.com/block/buzz/commit/3e76481a149bb3de459298cc989505c470c2372c))
|
||||
- fix(mobile): mirror app bar title padding when actions are empty ([#1832](https://github.com/block/buzz/pull/1832)) ([`f3599f2c`](https://github.com/block/buzz/commit/f3599f2cd4509e9120d8466d0ff2f79ee9dc5803))
|
||||
- docs(mobile): backfill mobile changelog ([#1835](https://github.com/block/buzz/pull/1835)) ([`259a1724`](https://github.com/block/buzz/commit/259a1724d31787a8a110e2689b8a813b22d0a382))
|
||||
- BOT-1247 Configure Android Play identity and signing ([#1829](https://github.com/block/buzz/pull/1829)) ([`f1706e23`](https://github.com/block/buzz/commit/f1706e23f79020812ef097c3167102a8ee3bfc9a))
|
||||
|
||||
|
||||
## mobile-v0.4.2
|
||||
|
||||
- fix(mobile): add mentioned agents to channels ([#1696](https://github.com/block/buzz/pull/1696)) ([`3112e59f`](https://github.com/block/buzz/commit/3112e59fd8141818712b04d55919145f0516846b))
|
||||
- Resynchronize the mobile version with the canonical release line after an accidental desktop-derived 0.4.1 version ([#1833](https://github.com/block/buzz/pull/1833)) ([`ffa2de0f`](https://github.com/block/buzz/commit/ffa2de0fba94438397429da7bf60e774285c7ddd))
|
||||
|
||||
|
||||
## mobile-v0.3.33
|
||||
|
||||
- fix(mobile): align Android AGP/Gradle/Kotlin with Flutter 3.41.7 (unbreak Android build) ([#1775](https://github.com/block/buzz/pull/1775)) ([`bc14052d`](https://github.com/block/buzz/commit/bc14052d3941ffcc4b54541f65b24b68ce8f215b))
|
||||
- Fix mobile relay reconnect lifecycle ([#1772](https://github.com/block/buzz/pull/1772)) ([`dbe7fb85`](https://github.com/block/buzz/commit/dbe7fb853486fe33d9df2660b29dee4574007cd1))
|
||||
- Serialize managed agent PATH tests ([#1777](https://github.com/block/buzz/pull/1777)) ([`1fc7488e`](https://github.com/block/buzz/commit/1fc7488ea58accf8bd96afe94cd93ee54174df00))
|
||||
- Fix oversized mobile Manage channel sheet ([#1774](https://github.com/block/buzz/pull/1774)) ([`81b55c9f`](https://github.com/block/buzz/commit/81b55c9f3eb147cf2b5753232ff75df6e51431f2))
|
||||
- fix(mobile): highlight full multi-word mentions ([#1762](https://github.com/block/buzz/pull/1762)) ([`92bb959e`](https://github.com/block/buzz/commit/92bb959ee1580ed05a45b80af599b53d78f611bb))
|
||||
- [BOT-1264] fix(mobile): #channel tags not tappable ([#1695](https://github.com/block/buzz/pull/1695)) ([`2687a2a1`](https://github.com/block/buzz/commit/2687a2a185a01bfb08478267587c08378d60136d))
|
||||
|
||||
|
||||
## mobile-v0.3.32
|
||||
|
||||
- Restrict the iOS app to iPhone ([#1735](https://github.com/block/buzz/pull/1735)) ([`215f5218`](https://github.com/block/buzz/commit/215f521880b29d421221b371b5a5818fcdaf4dea))
|
||||
|
||||
|
||||
## mobile-v0.3.31
|
||||
|
||||
- This release only resynchronized the pre-1.0 mobile version after an accidental 1.0.0 internal build ([#1724](https://github.com/block/buzz/pull/1724)) ([`bdb3e698`](https://github.com/block/buzz/commit/bdb3e6989988c2721e24ea37236edd60038eca50))
|
||||
@@ -0,0 +1,100 @@
|
||||
# Buzz Mobile
|
||||
|
||||
Flutter mobile client for Buzz.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
cd mobile
|
||||
flutter pub get
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
# From repo root (recommended — starts Docker, relay, and simulator):
|
||||
just mobile-dev
|
||||
|
||||
# Direct (requires services and relay already running):
|
||||
cd mobile && flutter run
|
||||
```
|
||||
|
||||
### Worktree-aware debug identity
|
||||
|
||||
Debug builds produced from a git worktree get a unique app identifier keyed
|
||||
to the **worktree directory name** (`com.buzz.buzzMobile.<slug>` on iOS,
|
||||
`xyz.block.buzz.mobile.<slug>` on Android) plus a display-only branch label
|
||||
in the app name (`Buzz (my-branch)`, or a short SHA when the worktree is
|
||||
detached). Because the identifier follows the directory rather than the
|
||||
branch, one worktree keeps exactly one installed app — and its login state —
|
||||
across branch switches, and builds from multiple worktrees install side by
|
||||
side, mirroring the desktop dev experience. Release and profile builds
|
||||
always keep the production identity and name.
|
||||
|
||||
`just mobile-dev` and `just mobile-build-android` apply this automatically by
|
||||
running `scripts/mobile-worktree-overrides.sh`, which writes two gitignored
|
||||
files:
|
||||
|
||||
- `mobile/ios/Flutter/WorktreeOverrides.xcconfig` (included by Debug builds
|
||||
only; a developer's `AppOverrides.xcconfig` is included after it, so
|
||||
app-specific overrides like a personal `BUNDLE_IDENTIFIER` for device
|
||||
signing always win)
|
||||
- `mobile/android/worktree.properties` (read by the debug build type only)
|
||||
|
||||
For direct Xcode / Android Studio / `flutter run` development, run
|
||||
`./scripts/mobile-worktree-overrides.sh` from the repo root once per branch
|
||||
switch to refresh the display label (the install identity never changes);
|
||||
the persisted files are then picked up by any subsequent build. In the main
|
||||
checkout the script is a no-op that removes stale override files, restoring
|
||||
the plain `Buzz` identity.
|
||||
|
||||
To remove leftover worktree-suffixed installs from booted iOS simulators and
|
||||
connected Android emulators, run `just mobile-clean` (add `--dry-run` via
|
||||
`./scripts/mobile-worktree-clean.sh --dry-run` to preview). Production
|
||||
installs are never touched.
|
||||
|
||||
## Checks
|
||||
|
||||
```bash
|
||||
dart format --output=none --set-exit-if-changed .
|
||||
flutter analyze
|
||||
flutter test
|
||||
```
|
||||
|
||||
Or from the repo root: `just mobile-check` and `just mobile-test`.
|
||||
|
||||
## Android release signing
|
||||
|
||||
Android release builds fail unless all upload-key inputs are supplied through the
|
||||
environment:
|
||||
|
||||
- `BUZZ_ANDROID_UPLOAD_KEYSTORE_PATH`: path to a CI-vended keystore file
|
||||
- `BUZZ_ANDROID_UPLOAD_KEYSTORE_PASSWORD`
|
||||
- `BUZZ_ANDROID_UPLOAD_KEY_ALIAS`
|
||||
- `BUZZ_ANDROID_UPLOAD_KEY_PASSWORD`
|
||||
|
||||
The keystore path must be absolute, and the keystore must remain outside the
|
||||
repository. Development and debug builds do not require these variables.
|
||||
|
||||
Release pipelines that sign through the central APK Signer service instead of
|
||||
a local upload keystore must set `BUZZ_ANDROID_RELEASE_SIGNING=external`. That
|
||||
mode produces an unsigned release bundle and refuses to run if any
|
||||
`BUZZ_ANDROID_UPLOAD_*` value is also set.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
lib/
|
||||
├── main.dart # Entry point, Riverpod bootstrap
|
||||
├── app.dart # MaterialApp with theme
|
||||
├── shared/
|
||||
│ └── theme/ # Catppuccin light/dark, spacing tokens, extensions
|
||||
└── features/
|
||||
└── home/ # Placeholder home surface
|
||||
```
|
||||
|
||||
- **State management:** Riverpod + Hooks (`HookConsumerWidget`)
|
||||
- **Theme:** Catppuccin Latte (light) / Macchiato (dark) — matches desktop
|
||||
- **Spacing:** `Grid` tokens for consistent spacing
|
||||
- **Linting:** `flutter_lints` + `riverpod_lint` via `custom_lint`
|
||||
- **Feature isolation:** No cross-feature imports except `shared/`
|
||||
@@ -0,0 +1,11 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
analyzer:
|
||||
plugins:
|
||||
- custom_lint
|
||||
errors:
|
||||
invalid_use_of_visible_for_overriding_member: ignore
|
||||
|
||||
linter:
|
||||
rules:
|
||||
prefer_single_quotes: true
|
||||
@@ -0,0 +1,15 @@
|
||||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
.cxx/
|
||||
/worktree.properties
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/to/reference-keystore
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
@@ -0,0 +1,176 @@
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
val uploadKeystorePath = providers.environmentVariable("BUZZ_ANDROID_UPLOAD_KEYSTORE_PATH").orNull
|
||||
val uploadKeystorePassword = providers.environmentVariable("BUZZ_ANDROID_UPLOAD_KEYSTORE_PASSWORD").orNull
|
||||
val uploadKeyAlias = providers.environmentVariable("BUZZ_ANDROID_UPLOAD_KEY_ALIAS").orNull
|
||||
val uploadKeyPassword = providers.environmentVariable("BUZZ_ANDROID_UPLOAD_KEY_PASSWORD").orNull
|
||||
val uploadSigningValues =
|
||||
mapOf(
|
||||
"BUZZ_ANDROID_UPLOAD_KEYSTORE_PATH" to uploadKeystorePath,
|
||||
"BUZZ_ANDROID_UPLOAD_KEYSTORE_PASSWORD" to uploadKeystorePassword,
|
||||
"BUZZ_ANDROID_UPLOAD_KEY_ALIAS" to uploadKeyAlias,
|
||||
"BUZZ_ANDROID_UPLOAD_KEY_PASSWORD" to uploadKeyPassword,
|
||||
)
|
||||
val missingUploadSigningValues = uploadSigningValues.filterValues { it.isNullOrBlank() }.keys
|
||||
val hasUploadSigning = missingUploadSigningValues.isEmpty()
|
||||
|
||||
// Worktree-aware debug identity (gitignored, written by
|
||||
// scripts/mobile-worktree-overrides.sh): debug builds from a git worktree get a
|
||||
// branch-labelled app name and a unique applicationId suffix so builds from
|
||||
// multiple worktrees install side by side. Release builds never read this.
|
||||
val worktreePropsFile = rootProject.file("worktree.properties")
|
||||
val worktreeProps =
|
||||
Properties().apply {
|
||||
if (worktreePropsFile.isFile) worktreePropsFile.inputStream().use { load(it) }
|
||||
}
|
||||
val worktreeLabel = worktreeProps.getProperty("label")?.takeIf { it.isNotBlank() }
|
||||
if (worktreeLabel != null && !worktreeLabel.matches(Regex("""[A-Za-z0-9._-]+"""))) {
|
||||
throw GradleException(
|
||||
"worktree.properties label must match [A-Za-z0-9._-]+ (safe for string " +
|
||||
"resources), got: " + worktreeLabel,
|
||||
)
|
||||
}
|
||||
val worktreeIdSuffix =
|
||||
worktreeProps.getProperty("applicationIdSuffix")?.takeIf { it.isNotBlank() }
|
||||
if (worktreeIdSuffix != null && !worktreeIdSuffix.matches(Regex("""\.[a-z][a-z0-9_]*"""))) {
|
||||
throw GradleException(
|
||||
"worktree.properties applicationIdSuffix must match \\.[a-z][a-z0-9_]*, got: " +
|
||||
worktreeIdSuffix,
|
||||
)
|
||||
}
|
||||
|
||||
// Release signing modes:
|
||||
// - "upload-keystore" (default): sign with the CI-vended upload keystore;
|
||||
// release builds fail loudly when any credential is missing.
|
||||
// - "external": deliberately produce an UNSIGNED release bundle for a
|
||||
// pipeline that signs through the central APK Signer service (Cashkite,
|
||||
// BOT-1234). No keystore material may be present in this mode.
|
||||
val releaseSigningMode =
|
||||
providers.environmentVariable("BUZZ_ANDROID_RELEASE_SIGNING").orNull ?: "upload-keystore"
|
||||
val externalReleaseSigning = releaseSigningMode == "external"
|
||||
if (releaseSigningMode !in setOf("upload-keystore", "external")) {
|
||||
throw GradleException(
|
||||
"BUZZ_ANDROID_RELEASE_SIGNING must be \"upload-keystore\" or \"external\", got: " +
|
||||
releaseSigningMode,
|
||||
)
|
||||
}
|
||||
if (externalReleaseSigning && uploadSigningValues.values.any { !it.isNullOrBlank() }) {
|
||||
throw GradleException(
|
||||
"BUZZ_ANDROID_RELEASE_SIGNING=external must not be combined with " +
|
||||
"BUZZ_ANDROID_UPLOAD_* credentials; unset one of them.",
|
||||
)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "xyz.block.buzz.mobile"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "xyz.block.buzz.mobile"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
resValue("string", "app_name", "Buzz")
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
if (hasUploadSigning) {
|
||||
create("upload") {
|
||||
storeFile = file(requireNotNull(uploadKeystorePath))
|
||||
storePassword = uploadKeystorePassword
|
||||
keyAlias = uploadKeyAlias
|
||||
keyPassword = uploadKeyPassword
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
// Only debug builds take the worktree identity; release/profile
|
||||
// keep the production applicationId and label.
|
||||
if (worktreeIdSuffix != null) {
|
||||
applicationIdSuffix = worktreeIdSuffix
|
||||
}
|
||||
if (worktreeLabel != null) {
|
||||
resValue("string", "app_name", "Buzz ($worktreeLabel)")
|
||||
}
|
||||
}
|
||||
release {
|
||||
if (hasUploadSigning) {
|
||||
signingConfig = signingConfigs.getByName("upload")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
|
||||
androidTestImplementation(kotlin("test"))
|
||||
androidTestImplementation("androidx.test.ext:junit:1.3.0")
|
||||
androidTestImplementation("androidx.test:runner:1.7.0")
|
||||
}
|
||||
|
||||
gradle.taskGraph.whenReady {
|
||||
val buildsRelease = allTasks.any { task ->
|
||||
task.project == project && task.name in setOf("assembleRelease", "bundleRelease")
|
||||
}
|
||||
if (buildsRelease && externalReleaseSigning) {
|
||||
// External signing: the unsigned bundle goes to the central APK
|
||||
// Signer. All keystore checks are intentionally skipped; the
|
||||
// guard above already rejected any BUZZ_ANDROID_UPLOAD_* values.
|
||||
return@whenReady
|
||||
}
|
||||
if (buildsRelease && !hasUploadSigning) {
|
||||
throw GradleException(
|
||||
"Release builds require Android upload signing credentials. Missing: " +
|
||||
missingUploadSigningValues.sorted().joinToString(", ") +
|
||||
". For central APK Signer pipelines set BUZZ_ANDROID_RELEASE_SIGNING=external.",
|
||||
)
|
||||
}
|
||||
if (buildsRelease) {
|
||||
val configuredKeystore = File(requireNotNull(uploadKeystorePath))
|
||||
if (!configuredKeystore.isAbsolute) {
|
||||
throw GradleException(
|
||||
"BUZZ_ANDROID_UPLOAD_KEYSTORE_PATH must be absolute: $configuredKeystore",
|
||||
)
|
||||
}
|
||||
val keystore = file(configuredKeystore)
|
||||
val repositoryRoot = rootProject.projectDir.parentFile.parentFile.canonicalFile
|
||||
if (keystore.canonicalFile.toPath().startsWith(repositoryRoot.toPath())) {
|
||||
throw GradleException(
|
||||
"BUZZ_ANDROID_UPLOAD_KEYSTORE_PATH must be outside the repository: $keystore",
|
||||
)
|
||||
}
|
||||
if (!keystore.isFile || !keystore.canRead()) {
|
||||
throw GradleException(
|
||||
"BUZZ_ANDROID_UPLOAD_KEYSTORE_PATH is not a readable file: $keystore",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package xyz.block.buzz.mobile
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import kotlin.math.abs
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AndroidImageProcessorTest {
|
||||
companion object {
|
||||
private val ALLOWED_PNG_CHUNKS = setOf(
|
||||
"IHDR", "PLTE", "IDAT", "IEND", "cHRM", "gAMA", "sBIT", "sRGB", "bKGD", "hIST", "tRNS", "sPLT",
|
||||
"acTL", "fcTL", "fdAT",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun supportedApiCanProcessImagesWithoutNewerPlatformApis() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) return
|
||||
|
||||
val sourceBytes = fixtureBytes("bitmap-srgb.png")
|
||||
val processed = assertNotNull(AndroidImageProcessor.decodeSrgbBitmap(sourceBytes))
|
||||
assertEquals(Bitmap.Config.ARGB_8888, processed.config)
|
||||
|
||||
val scrubbed = assertNotNull(
|
||||
AndroidImageProcessor.encodeAndScrub(processed, Bitmap.CompressFormat.PNG),
|
||||
)
|
||||
val chunkTypes = pngChunkTypes(scrubbed)
|
||||
assertEquals("IHDR", chunkTypes.first())
|
||||
assertEquals("IEND", chunkTypes.last())
|
||||
assertTrue(chunkTypes.all { it in ALLOWED_PNG_CHUNKS })
|
||||
assertPngPixelsPreserved(sourceBytes, scrubbed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun transparentPngPixelsSurviveSrgbProcessing() {
|
||||
val sourceBytes = fixtureBytes("bitmap-srgb.png")
|
||||
val source = assertNotNull(BitmapFactory.decodeByteArray(sourceBytes, 0, sourceBytes.size))
|
||||
val processed = assertNotNull(AndroidImageProcessor.decodeSrgbBitmap(sourceBytes))
|
||||
assertTrue(processed.hasAlpha())
|
||||
|
||||
val scrubbed = assertNotNull(
|
||||
AndroidImageProcessor.encodeAndScrub(processed, Bitmap.CompressFormat.PNG),
|
||||
)
|
||||
assertPngPixelsPreserved(sourceBytes, scrubbed)
|
||||
assertEquals(128, Color.alpha(source.getPixel(0, 1)))
|
||||
assertEquals(64, Color.alpha(source.getPixel(1, 1)))
|
||||
assertEquals(0, Color.alpha(source.getPixel(2, 1)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun displayP3InputIsConvertedToSrgbBeforeEncodedMetadataIsScrubbed() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
|
||||
val sourceBytes = fixtureBytes("bitmap-display-p3.png")
|
||||
val source = assertNotNull(BitmapFactory.decodeByteArray(sourceBytes, 0, sourceBytes.size))
|
||||
assertEquals(android.graphics.ColorSpace.get(android.graphics.ColorSpace.Named.DISPLAY_P3), source.colorSpace)
|
||||
val expectedSrgbPixel = source.getColor(0, 0).convert(
|
||||
android.graphics.ColorSpace.get(android.graphics.ColorSpace.Named.SRGB),
|
||||
)
|
||||
|
||||
val srgbBitmap = assertNotNull(AndroidImageProcessor.decodeSrgbBitmap(sourceBytes))
|
||||
assertEquals(android.graphics.ColorSpace.get(android.graphics.ColorSpace.Named.SRGB), srgbBitmap.colorSpace)
|
||||
assertEquals(Bitmap.Config.ARGB_8888, srgbBitmap.config)
|
||||
assertColorClose(expectedSrgbPixel, srgbBitmap.getColor(0, 0), tolerance = 1.0f / 255.0f)
|
||||
|
||||
for ((format, tolerance) in listOf(
|
||||
Bitmap.CompressFormat.PNG to (1.0f / 255.0f),
|
||||
Bitmap.CompressFormat.JPEG to (2.0f / 255.0f),
|
||||
)) {
|
||||
val scrubbed = assertNotNull(AndroidImageProcessor.encodeAndScrub(srgbBitmap, format))
|
||||
when (format) {
|
||||
Bitmap.CompressFormat.PNG -> {
|
||||
assertEquals(listOf("IHDR", "sRGB", "sBIT", "IDAT", "IEND"), pngChunkTypes(scrubbed))
|
||||
}
|
||||
Bitmap.CompressFormat.JPEG -> {
|
||||
assertEquals(listOf(0xE0), jpegMetadataMarkers(scrubbed))
|
||||
}
|
||||
else -> error("Unexpected format: $format")
|
||||
}
|
||||
|
||||
val decodedOutput = assertNotNull(BitmapFactory.decodeByteArray(scrubbed, 0, scrubbed.size))
|
||||
assertEquals(
|
||||
android.graphics.ColorSpace.get(android.graphics.ColorSpace.Named.SRGB),
|
||||
decodedOutput.colorSpace,
|
||||
)
|
||||
assertColorClose(expectedSrgbPixel, decodedOutput.getColor(0, 0), tolerance)
|
||||
}
|
||||
}
|
||||
|
||||
private fun assertPngPixelsPreserved(expectedBytes: ByteArray, actualBytes: ByteArray) {
|
||||
val expected = assertNotNull(BitmapFactory.decodeByteArray(expectedBytes, 0, expectedBytes.size))
|
||||
val actual = assertNotNull(BitmapFactory.decodeByteArray(actualBytes, 0, actualBytes.size))
|
||||
assertEquals(expected.width, actual.width)
|
||||
assertEquals(expected.height, actual.height)
|
||||
for (y in 0 until expected.height) {
|
||||
for (x in 0 until expected.width) {
|
||||
assertEquals(
|
||||
expected.getPixel(x, y),
|
||||
actual.getPixel(x, y),
|
||||
"pixel ($x, $y)",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fixtureBytes(name: String): ByteArray {
|
||||
return requireNotNull(javaClass.getResourceAsStream("/fixtures/android/$name")) {
|
||||
"Missing fixture: $name"
|
||||
}.use { it.readBytes() }
|
||||
}
|
||||
|
||||
private fun assertColorClose(expected: Color, actual: Color, tolerance: Float) {
|
||||
val expectedComponents = expected.components
|
||||
val actualComponents = actual.convert(expected.colorSpace).components
|
||||
for (index in expectedComponents.indices) {
|
||||
assertTrue(
|
||||
abs(expectedComponents[index] - actualComponents[index]) <= tolerance,
|
||||
"component $index: expected ${expectedComponents[index]}, got ${actualComponents[index]}",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun pngChunkTypes(bytes: ByteArray): List<String> {
|
||||
val result = mutableListOf<String>()
|
||||
var offset = 8
|
||||
while (offset < bytes.size) {
|
||||
require(bytes.size - offset >= 12)
|
||||
val payloadLength = readUnsignedInt(bytes, offset)
|
||||
val type = bytes.decodeToString(offset + 4, offset + 8)
|
||||
result += type
|
||||
offset += payloadLength + 12
|
||||
if (type == "IEND") {
|
||||
assertEquals(bytes.size, offset)
|
||||
return result
|
||||
}
|
||||
}
|
||||
error("PNG is missing IEND")
|
||||
}
|
||||
|
||||
private fun jpegMetadataMarkers(bytes: ByteArray): List<Int> {
|
||||
val result = mutableListOf<Int>()
|
||||
var offset = 2
|
||||
var inScan = false
|
||||
while (offset < bytes.size) {
|
||||
if (inScan && bytes[offset] != 0xFF.toByte()) {
|
||||
offset += 1
|
||||
continue
|
||||
}
|
||||
require(bytes[offset] == 0xFF.toByte())
|
||||
while (offset < bytes.size && bytes[offset] == 0xFF.toByte()) {
|
||||
offset += 1
|
||||
}
|
||||
require(offset < bytes.size)
|
||||
val marker = bytes[offset].toInt() and 0xFF
|
||||
offset += 1
|
||||
if (inScan && marker == 0x00) continue
|
||||
if (marker in 0xD0..0xD7 || marker == 0x01) continue
|
||||
if (marker == 0xD9) {
|
||||
assertEquals(bytes.size, offset)
|
||||
return result
|
||||
}
|
||||
val segmentLength = readUnsignedShort(bytes, offset)
|
||||
if (marker in 0xE0..0xEF || marker == 0xFE) result += marker
|
||||
offset += segmentLength
|
||||
inScan = marker == 0xDA
|
||||
}
|
||||
error("JPEG is missing EOI")
|
||||
}
|
||||
|
||||
private fun readUnsignedShort(bytes: ByteArray, offset: Int): Int {
|
||||
return ((bytes[offset].toInt() and 0xFF) shl 8) or (bytes[offset + 1].toInt() and 0xFF)
|
||||
}
|
||||
|
||||
private fun readUnsignedInt(bytes: ByteArray, offset: Int): Int {
|
||||
return ((bytes[offset].toInt() and 0xFF) shl 24) or
|
||||
((bytes[offset + 1].toInt() and 0xFF) shl 16) or
|
||||
((bytes[offset + 2].toInt() and 0xFF) shl 8) or
|
||||
(bytes[offset + 3].toInt() and 0xFF)
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 407 B |
|
After Width: | Height: | Size: 112 B |
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,71 @@
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32"
|
||||
tools:replace="android:maxSdkVersion" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
|
||||
<application
|
||||
android:label="@string/app_name"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<!-- Handle buzz:// deep links (e.g. buzz://message?channel=…&id=…). -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="buzz"/>
|
||||
</intent-filter>
|
||||
<!-- Disable Flutter's built-in deeplink handler; app_links owns
|
||||
incoming URLs so they can be routed with app state in Dart. -->
|
||||
<meta-data
|
||||
android:name="flutter_deeplinking_enabled"
|
||||
android:value="false" />
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
@@ -0,0 +1,160 @@
|
||||
package xyz.block.buzz.mobile
|
||||
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
internal object AndroidMediaSanitizer {
|
||||
private val pngSignature = byteArrayOf(
|
||||
0x89.toByte(), 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
|
||||
)
|
||||
private val allowedPngAncillaryChunks = setOf(
|
||||
"cHRM", "gAMA", "sBIT", "sRGB", "bKGD", "hIST", "tRNS", "sPLT", "acTL", "fcTL", "fdAT",
|
||||
)
|
||||
|
||||
fun scrubPng(bytes: ByteArray): ByteArray {
|
||||
require(bytes.size >= pngSignature.size && bytes.copyOfRange(0, pngSignature.size).contentEquals(pngSignature)) {
|
||||
"Invalid PNG signature"
|
||||
}
|
||||
|
||||
val output = ByteArrayOutputStream(bytes.size)
|
||||
output.write(pngSignature)
|
||||
var offset = pngSignature.size
|
||||
while (offset < bytes.size) {
|
||||
require(bytes.size - offset >= PNG_CHUNK_OVERHEAD) { "Truncated PNG chunk" }
|
||||
val payloadLength = readUnsignedIntBigEndian(bytes, offset)
|
||||
require(payloadLength <= bytes.size.toLong() - offset - PNG_CHUNK_OVERHEAD) {
|
||||
"Invalid PNG chunk length"
|
||||
}
|
||||
val chunkLength = payloadLength.toInt() + PNG_CHUNK_OVERHEAD
|
||||
val typeStart = offset + 4
|
||||
val type = String(bytes, typeStart, 4, StandardCharsets.US_ASCII)
|
||||
val isAncillary = bytes[typeStart].toInt() and 0x20 != 0
|
||||
if (!isAncillary || type in allowedPngAncillaryChunks) {
|
||||
output.write(bytes, offset, chunkLength)
|
||||
}
|
||||
offset += chunkLength
|
||||
if (type == "IEND") {
|
||||
return output.toByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
throw IllegalArgumentException("PNG is missing IEND")
|
||||
}
|
||||
|
||||
fun scrubJpeg(bytes: ByteArray): ByteArray {
|
||||
require(
|
||||
bytes.size >= 2 &&
|
||||
bytes[0] == 0xFF.toByte() &&
|
||||
(bytes[1].toInt() and 0xFF) == JPEG_SOI,
|
||||
) {
|
||||
"Invalid JPEG signature"
|
||||
}
|
||||
|
||||
val output = ByteArrayOutputStream(bytes.size)
|
||||
output.write(byteArrayOf(0xFF.toByte(), JPEG_SOI.toByte()))
|
||||
var offset = 2
|
||||
var inScan = false
|
||||
while (offset < bytes.size) {
|
||||
if (inScan && bytes[offset] != 0xFF.toByte()) {
|
||||
val nextMarker = bytes.indexOf(0xFF.toByte(), offset).let { if (it == -1) bytes.size else it }
|
||||
output.write(bytes, offset, nextMarker - offset)
|
||||
offset = nextMarker
|
||||
continue
|
||||
}
|
||||
require(bytes[offset] == 0xFF.toByte()) { "Invalid JPEG marker" }
|
||||
|
||||
val markerStart = offset
|
||||
while (offset < bytes.size && bytes[offset] == 0xFF.toByte()) {
|
||||
offset += 1
|
||||
}
|
||||
require(offset < bytes.size) { "Truncated JPEG marker" }
|
||||
|
||||
val marker = bytes[offset].toInt() and 0xFF
|
||||
offset += 1
|
||||
if (inScan && marker == 0x00) {
|
||||
output.write(bytes, markerStart, offset - markerStart)
|
||||
continue
|
||||
}
|
||||
if (marker in JPEG_RESTART_MARKERS || marker == JPEG_TEMP) {
|
||||
output.write(bytes, markerStart, offset - markerStart)
|
||||
continue
|
||||
}
|
||||
if (marker == JPEG_EOI) {
|
||||
output.write(bytes, markerStart, offset - markerStart)
|
||||
return output.toByteArray()
|
||||
}
|
||||
require(marker != JPEG_SOI && bytes.size - offset >= 2) { "Invalid JPEG segment" }
|
||||
|
||||
val segmentLength = readUnsignedShortBigEndian(bytes, offset)
|
||||
require(segmentLength >= 2 && segmentLength <= bytes.size - offset) { "Invalid JPEG segment length" }
|
||||
val segmentEnd = offset + segmentLength
|
||||
if (shouldKeepJpegSegment(marker, bytes, offset + 2, segmentEnd)) {
|
||||
output.write(bytes, markerStart, segmentEnd - markerStart)
|
||||
}
|
||||
offset = segmentEnd
|
||||
inScan = marker == JPEG_SOS
|
||||
}
|
||||
|
||||
throw IllegalArgumentException("JPEG is missing EOI")
|
||||
}
|
||||
|
||||
private fun shouldKeepJpegSegment(
|
||||
marker: Int,
|
||||
bytes: ByteArray,
|
||||
payloadStart: Int,
|
||||
payloadEnd: Int,
|
||||
): Boolean {
|
||||
val payloadLength = payloadEnd - payloadStart
|
||||
return when (marker) {
|
||||
JPEG_APP0 -> {
|
||||
if (payloadLength < 14 || !bytes.matchesAscii(payloadStart, "JFIF\u0000")) {
|
||||
false
|
||||
} else {
|
||||
val thumbnailWidth = bytes[payloadStart + 12].toInt() and 0xFF
|
||||
val thumbnailHeight = bytes[payloadStart + 13].toInt() and 0xFF
|
||||
payloadLength == 14 + 3 * thumbnailWidth * thumbnailHeight
|
||||
}
|
||||
}
|
||||
JPEG_APP14 -> payloadLength == 12 && bytes.matchesAscii(payloadStart, "Adobe")
|
||||
in JPEG_FORBIDDEN_APP_MARKERS, JPEG_APP15, JPEG_COMMENT -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
private fun readUnsignedShortBigEndian(bytes: ByteArray, offset: Int): Int {
|
||||
require(bytes.size - offset >= 2) { "Truncated two-byte integer" }
|
||||
return (bytes[offset].toInt() and 0xFF shl 8) or (bytes[offset + 1].toInt() and 0xFF)
|
||||
}
|
||||
|
||||
private fun readUnsignedIntBigEndian(bytes: ByteArray, offset: Int): Long {
|
||||
require(bytes.size - offset >= 4) { "Truncated four-byte integer" }
|
||||
return (bytes[offset].toLong() and 0xFF shl 24) or
|
||||
(bytes[offset + 1].toLong() and 0xFF shl 16) or
|
||||
(bytes[offset + 2].toLong() and 0xFF shl 8) or
|
||||
(bytes[offset + 3].toLong() and 0xFF)
|
||||
}
|
||||
|
||||
private fun ByteArray.matchesAscii(offset: Int, value: String): Boolean {
|
||||
val expected = value.toByteArray(StandardCharsets.US_ASCII)
|
||||
return size - offset >= expected.size && expected.indices.all { this[offset + it] == expected[it] }
|
||||
}
|
||||
|
||||
private fun ByteArray.indexOf(value: Byte, startIndex: Int): Int {
|
||||
for (index in startIndex until size) {
|
||||
if (this[index] == value) return index
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
private const val PNG_CHUNK_OVERHEAD = 12
|
||||
private const val JPEG_SOI = 0xD8
|
||||
private const val JPEG_EOI = 0xD9
|
||||
private const val JPEG_SOS = 0xDA
|
||||
private const val JPEG_TEMP = 0x01
|
||||
private const val JPEG_APP0 = 0xE0
|
||||
private const val JPEG_APP14 = 0xEE
|
||||
private const val JPEG_APP15 = 0xEF
|
||||
private const val JPEG_COMMENT = 0xFE
|
||||
private val JPEG_RESTART_MARKERS = 0xD0..0xD7
|
||||
private val JPEG_FORBIDDEN_APP_MARKERS = 0xE1..0xED
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
package xyz.block.buzz.mobile
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.ColorSpace
|
||||
import android.graphics.ImageDecoder
|
||||
import android.media.MediaExtractor
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.media.MediaMuxer
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.UUID
|
||||
|
||||
internal object AndroidImageProcessor {
|
||||
fun decodeSrgbBitmap(bytes: ByteArray): Bitmap? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
decodeSrgbBitmapWithColorManagement(bytes)
|
||||
} else {
|
||||
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun decodeSrgbBitmapWithColorManagement(bytes: ByteArray): Bitmap? {
|
||||
val decoded = decodeColorManagedBitmap(bytes) ?: return null
|
||||
val srgb = ColorSpace.get(ColorSpace.Named.SRGB)
|
||||
if (decoded.config == Bitmap.Config.ARGB_8888 && decoded.colorSpace == srgb) return decoded
|
||||
|
||||
val srgbBitmap = Bitmap.createBitmap(
|
||||
decoded.width,
|
||||
decoded.height,
|
||||
Bitmap.Config.ARGB_8888,
|
||||
decoded.hasAlpha(),
|
||||
srgb,
|
||||
)
|
||||
Canvas(srgbBitmap).drawBitmap(decoded, 0f, 0f, null)
|
||||
return srgbBitmap
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun decodeColorManagedBitmap(bytes: ByteArray): Bitmap? {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
runCatching {
|
||||
val source = ImageDecoder.createSource(ByteBuffer.wrap(bytes))
|
||||
ImageDecoder.decodeBitmap(source) { decoder, _, _ ->
|
||||
decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
|
||||
decoder.setTargetColorSpace(ColorSpace.get(ColorSpace.Named.SRGB))
|
||||
}
|
||||
}.getOrNull()?.let { return it }
|
||||
}
|
||||
|
||||
val options = BitmapFactory.Options().apply {
|
||||
inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.SRGB)
|
||||
}
|
||||
return BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options)
|
||||
}
|
||||
|
||||
fun encodeAndScrub(
|
||||
bitmap: Bitmap,
|
||||
format: Bitmap.CompressFormat,
|
||||
): ByteArray? {
|
||||
val output = ByteArrayOutputStream()
|
||||
if (!bitmap.compress(format, 100, output)) return null
|
||||
|
||||
return when (format) {
|
||||
Bitmap.CompressFormat.PNG -> AndroidMediaSanitizer.scrubPng(output.toByteArray())
|
||||
Bitmap.CompressFormat.JPEG -> AndroidMediaSanitizer.scrubJpeg(output.toByteArray())
|
||||
else -> error("Unsupported upload image format: $format")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
private var mediaUploadChannel: MethodChannel? = null
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
mediaUploadChannel = MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
MEDIA_UPLOAD_CHANNEL,
|
||||
).also { channel ->
|
||||
channel.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
SANITIZE_IMAGE_FOR_UPLOAD_METHOD -> {
|
||||
handleSanitizeImageForUpload(call.arguments, result)
|
||||
}
|
||||
TRANSCODE_IMAGE_TO_JPEG_METHOD -> {
|
||||
handleTranscodeImageToJpeg(call.arguments, result)
|
||||
}
|
||||
TRANSCODE_VIDEO_TO_MP4_METHOD -> {
|
||||
handleTranscodeVideoToMp4(call.arguments, result)
|
||||
}
|
||||
GENERATE_VIDEO_POSTER_METHOD -> {
|
||||
handleGenerateVideoPoster(call.arguments, result)
|
||||
}
|
||||
REQUIRES_LEGACY_MEDIA_STORAGE_PERMISSION_METHOD -> {
|
||||
result.success(Build.VERSION.SDK_INT <= Build.VERSION_CODES.P)
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSanitizeImageForUpload(
|
||||
arguments: Any?,
|
||||
result: MethodChannel.Result,
|
||||
) {
|
||||
val payload = arguments as? Map<*, *> ?: run {
|
||||
invalidArguments(result, "Expected image bytes and mime type.")
|
||||
return
|
||||
}
|
||||
val bytes = payload["bytes"] as? ByteArray ?: run {
|
||||
invalidArguments(result, "Expected raw image bytes.")
|
||||
return
|
||||
}
|
||||
val mimeType = payload["mimeType"] as? String ?: run {
|
||||
invalidArguments(result, "Expected image mime type.")
|
||||
return
|
||||
}
|
||||
|
||||
val format = sanitizeCompressFormatFor(mimeType)
|
||||
if (format == null) {
|
||||
result.error(
|
||||
"sanitize_failed",
|
||||
"Unable to sanitize picked image.",
|
||||
mimeType,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
transformImageBytes(
|
||||
bytes = bytes,
|
||||
result = result,
|
||||
format = format,
|
||||
errorCode = "sanitize_failed",
|
||||
encodeFailureMessage = "Unable to sanitize picked image.",
|
||||
errorDetails = mimeType,
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleTranscodeImageToJpeg(
|
||||
arguments: Any?,
|
||||
result: MethodChannel.Result,
|
||||
) {
|
||||
val bytes = arguments as? ByteArray ?: run {
|
||||
invalidArguments(result, "Expected raw image bytes.")
|
||||
return
|
||||
}
|
||||
|
||||
transformImageBytes(
|
||||
bytes = bytes,
|
||||
result = result,
|
||||
format = Bitmap.CompressFormat.JPEG,
|
||||
errorCode = "transcode_failed",
|
||||
encodeFailureMessage = "Unable to convert picked image to JPEG.",
|
||||
)
|
||||
}
|
||||
|
||||
private fun sanitizeCompressFormatFor(
|
||||
mimeType: String,
|
||||
): Bitmap.CompressFormat? {
|
||||
return when (mimeType) {
|
||||
"image/jpeg" -> Bitmap.CompressFormat.JPEG
|
||||
"image/png", "image/webp" -> Bitmap.CompressFormat.PNG
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun transformImageBytes(
|
||||
bytes: ByteArray,
|
||||
result: MethodChannel.Result,
|
||||
format: Bitmap.CompressFormat,
|
||||
errorCode: String,
|
||||
encodeFailureMessage: String,
|
||||
errorDetails: Any? = null,
|
||||
) {
|
||||
val bitmap = AndroidImageProcessor.decodeSrgbBitmap(bytes) ?: run {
|
||||
result.error(
|
||||
errorCode,
|
||||
"Unable to decode picked image.",
|
||||
null,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val transformedBytes = try {
|
||||
AndroidImageProcessor.encodeAndScrub(bitmap, format)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
null
|
||||
} ?: run {
|
||||
result.error(
|
||||
errorCode,
|
||||
encodeFailureMessage,
|
||||
errorDetails,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
result.success(transformedBytes)
|
||||
}
|
||||
|
||||
private fun handleTranscodeVideoToMp4(
|
||||
arguments: Any?,
|
||||
result: MethodChannel.Result,
|
||||
) {
|
||||
val sourcePath = arguments as? String ?: run {
|
||||
invalidArguments(result, "Expected source file path as String.")
|
||||
return
|
||||
}
|
||||
|
||||
Thread {
|
||||
val outputFile = File(cacheDir, "${UUID.randomUUID()}.mp4")
|
||||
var muxer: MediaMuxer? = null
|
||||
val extractor = MediaExtractor()
|
||||
try {
|
||||
extractor.setDataSource(sourcePath)
|
||||
muxer = MediaMuxer(outputFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
|
||||
|
||||
val trackIndices = mutableMapOf<Int, Int>()
|
||||
var copiedVideo = false
|
||||
var copiedAudio = false
|
||||
for (i in 0 until extractor.trackCount) {
|
||||
val format = extractor.getTrackFormat(i)
|
||||
val mime = format.getString(android.media.MediaFormat.KEY_MIME) ?: continue
|
||||
val isVideo = mime.startsWith("video/")
|
||||
val isAudio = mime.startsWith("audio/")
|
||||
if ((!isVideo && !isAudio) || (isVideo && copiedVideo) || (isAudio && copiedAudio)) {
|
||||
continue
|
||||
}
|
||||
val newIndex = muxer.addTrack(format)
|
||||
trackIndices[i] = newIndex
|
||||
extractor.selectTrack(i)
|
||||
copiedVideo = copiedVideo || isVideo
|
||||
copiedAudio = copiedAudio || isAudio
|
||||
}
|
||||
|
||||
muxer.start()
|
||||
val buffer = ByteBuffer.allocate(1024 * 1024) // 1MB buffer
|
||||
val bufferInfo = android.media.MediaCodec.BufferInfo()
|
||||
|
||||
while (true) {
|
||||
val sampleSize = extractor.readSampleData(buffer, 0)
|
||||
if (sampleSize < 0) break
|
||||
val muxerTrack = trackIndices[extractor.sampleTrackIndex]
|
||||
if (muxerTrack == null) {
|
||||
extractor.advance()
|
||||
continue
|
||||
}
|
||||
bufferInfo.offset = 0
|
||||
bufferInfo.size = sampleSize
|
||||
bufferInfo.presentationTimeUs = extractor.sampleTime
|
||||
bufferInfo.flags = extractor.sampleFlags
|
||||
muxer.writeSampleData(muxerTrack, buffer, bufferInfo)
|
||||
extractor.advance()
|
||||
}
|
||||
|
||||
muxer.stop()
|
||||
result.success(outputFile.absolutePath)
|
||||
} catch (e: Exception) {
|
||||
outputFile.delete()
|
||||
result.error(
|
||||
"transcode_failed",
|
||||
e.message ?: "Video transcoding failed.",
|
||||
null,
|
||||
)
|
||||
} finally {
|
||||
try { muxer?.release() } catch (_: Exception) {}
|
||||
extractor.release()
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun handleGenerateVideoPoster(
|
||||
arguments: Any?,
|
||||
result: MethodChannel.Result,
|
||||
) {
|
||||
val sourcePath = arguments as? String ?: run {
|
||||
invalidArguments(result, "Expected source file path as String.")
|
||||
return
|
||||
}
|
||||
|
||||
Thread {
|
||||
val retriever = MediaMetadataRetriever()
|
||||
try {
|
||||
retriever.setDataSource(sourcePath)
|
||||
val source = retriever.getFrameAtTime(
|
||||
0,
|
||||
MediaMetadataRetriever.OPTION_CLOSEST_SYNC,
|
||||
) ?: retriever.getFrameAtTime(
|
||||
100_000,
|
||||
MediaMetadataRetriever.OPTION_CLOSEST_SYNC,
|
||||
) ?: throw IllegalArgumentException("Unable to decode a video frame.")
|
||||
val scale = minOf(1f, 720f / maxOf(source.width, source.height))
|
||||
val frame = if (scale < 1f) {
|
||||
Bitmap.createScaledBitmap(
|
||||
source,
|
||||
(source.width * scale).toInt(),
|
||||
(source.height * scale).toInt(),
|
||||
true,
|
||||
).also { source.recycle() }
|
||||
} else {
|
||||
source
|
||||
}
|
||||
val bytes = AndroidImageProcessor.encodeAndScrub(
|
||||
frame,
|
||||
Bitmap.CompressFormat.JPEG,
|
||||
) ?: throw IllegalArgumentException("Unable to encode a video preview.")
|
||||
frame.recycle()
|
||||
result.success(bytes)
|
||||
} catch (e: Exception) {
|
||||
result.error(
|
||||
"poster_failed",
|
||||
"Unable to create a video preview.",
|
||||
e.message,
|
||||
)
|
||||
} finally {
|
||||
retriever.release()
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun invalidArguments(
|
||||
result: MethodChannel.Result,
|
||||
message: String,
|
||||
) {
|
||||
result.error("invalid_arguments", message, null)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MEDIA_UPLOAD_CHANNEL = "buzz/media_upload"
|
||||
private const val SANITIZE_IMAGE_FOR_UPLOAD_METHOD = "sanitizeImageForUpload"
|
||||
private const val TRANSCODE_IMAGE_TO_JPEG_METHOD = "transcodeImageToJpeg"
|
||||
private const val TRANSCODE_VIDEO_TO_MP4_METHOD = "transcodeVideoToMp4"
|
||||
private const val GENERATE_VIDEO_POSTER_METHOD = "generateVideoPoster"
|
||||
private const val REQUIRES_LEGACY_MEDIA_STORAGE_PERMISSION_METHOD =
|
||||
"requiresLegacyMediaStoragePermission"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/black" />
|
||||
|
||||
<item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<inset xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:drawable="@mipmap/ic_launcher_foreground"
|
||||
android:inset="10%" />
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/black" />
|
||||
|
||||
<item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground_inset"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 48 KiB |
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#000</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,128 @@
|
||||
package xyz.block.buzz.mobile
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class AndroidMediaSanitizerTest {
|
||||
@Test
|
||||
fun `scrubPng keeps canonical sRGB output unchanged`() {
|
||||
val fixture = fixtureBytes("bitmap-srgb.png")
|
||||
|
||||
val sanitized = AndroidMediaSanitizer.scrubPng(fixture)
|
||||
|
||||
assertContentEquals(fixture, sanitized)
|
||||
assertEquals(listOf("IHDR", "sRGB", "sBIT", "IDAT", "IEND"), pngChunkTypes(sanitized))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `scrubPng removes Display P3 profile and trailing data`() {
|
||||
val fixture = fixtureBytes("bitmap-display-p3.png")
|
||||
val withTrailingData = fixture + "hidden location".encodeToByteArray()
|
||||
|
||||
val sanitized = AndroidMediaSanitizer.scrubPng(withTrailingData)
|
||||
|
||||
assertEquals(listOf("IHDR", "sBIT", "IDAT", "IEND"), pngChunkTypes(sanitized))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `scrubJpeg removes Android ICC profile and trailing data`() {
|
||||
for (fixtureName in listOf("bitmap-srgb.jpg", "bitmap-display-p3.jpg")) {
|
||||
val fixture = fixtureBytes(fixtureName)
|
||||
val withTrailingData = fixture + "hidden location".encodeToByteArray()
|
||||
|
||||
val sanitized = AndroidMediaSanitizer.scrubJpeg(withTrailingData)
|
||||
|
||||
assertEquals(listOf(0xE0), jpegMetadataMarkers(sanitized), fixtureName)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `scrubbers fail closed for malformed containers`() {
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
AndroidMediaSanitizer.scrubPng(byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47))
|
||||
}
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
AndroidMediaSanitizer.scrubJpeg(byteArrayOf(0xFF.toByte(), 0xD8.toByte(), 0xFF.toByte()))
|
||||
}
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
AndroidMediaSanitizer.scrubJpeg(
|
||||
byteArrayOf(
|
||||
0xFF.toByte(),
|
||||
0xD8.toByte(),
|
||||
0xFF.toByte(),
|
||||
0xD8.toByte(),
|
||||
0x00,
|
||||
0x02,
|
||||
0xFF.toByte(),
|
||||
0xD9.toByte(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fixtureBytes(name: String): ByteArray {
|
||||
return requireNotNull(javaClass.getResourceAsStream("/fixtures/android/$name")) {
|
||||
"Missing fixture: $name"
|
||||
}.use { it.readBytes() }
|
||||
}
|
||||
|
||||
private fun pngChunkTypes(bytes: ByteArray): List<String> {
|
||||
val result = mutableListOf<String>()
|
||||
var offset = 8
|
||||
while (offset < bytes.size) {
|
||||
require(bytes.size - offset >= 12)
|
||||
val payloadLength = readUnsignedInt(bytes, offset)
|
||||
val type = bytes.decodeToString(offset + 4, offset + 8)
|
||||
result += type
|
||||
offset += payloadLength + 12
|
||||
if (type == "IEND") {
|
||||
assertEquals(bytes.size, offset)
|
||||
return result
|
||||
}
|
||||
}
|
||||
error("PNG is missing IEND")
|
||||
}
|
||||
|
||||
private fun jpegMetadataMarkers(bytes: ByteArray): List<Int> {
|
||||
val result = mutableListOf<Int>()
|
||||
var offset = 2
|
||||
var inScan = false
|
||||
while (offset < bytes.size) {
|
||||
if (inScan && bytes[offset] != 0xFF.toByte()) {
|
||||
offset += 1
|
||||
continue
|
||||
}
|
||||
require(bytes[offset] == 0xFF.toByte())
|
||||
while (offset < bytes.size && bytes[offset] == 0xFF.toByte()) {
|
||||
offset += 1
|
||||
}
|
||||
require(offset < bytes.size)
|
||||
val marker = bytes[offset].toInt() and 0xFF
|
||||
offset += 1
|
||||
if (inScan && marker == 0x00) continue
|
||||
if (marker in 0xD0..0xD7 || marker == 0x01) continue
|
||||
if (marker == 0xD9) {
|
||||
assertEquals(bytes.size, offset)
|
||||
return result
|
||||
}
|
||||
val segmentLength = readUnsignedShort(bytes, offset)
|
||||
if (marker in 0xE0..0xEF || marker == 0xFE) result += marker
|
||||
offset += segmentLength
|
||||
inScan = marker == 0xDA
|
||||
}
|
||||
error("JPEG is missing EOI")
|
||||
}
|
||||
|
||||
private fun readUnsignedShort(bytes: ByteArray, offset: Int): Int {
|
||||
return ((bytes[offset].toInt() and 0xFF) shl 8) or (bytes[offset + 1].toInt() and 0xFF)
|
||||
}
|
||||
|
||||
private fun readUnsignedInt(bytes: ByteArray, offset: Int): Int {
|
||||
return ((bytes[offset].toInt() and 0xFF) shl 24) or
|
||||
((bytes[offset + 1].toInt() and 0xFF) shl 16) or
|
||||
((bytes[offset + 2].toInt() and 0xFF) shl 8) or
|
||||
(bytes[offset + 3].toInt() and 0xFF)
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 827 B |
|
After Width: | Height: | Size: 407 B |
|
After Width: | Height: | Size: 857 B |
|
After Width: | Height: | Size: 112 B |
@@ -0,0 +1,24 @@
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
val newBuildDir: Directory =
|
||||
rootProject.layout.buildDirectory
|
||||
.dir("../../build")
|
||||
.get()
|
||||
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||
|
||||
subprojects {
|
||||
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-all.zip
|
||||
@@ -0,0 +1,26 @@
|
||||
pluginManagement {
|
||||
val flutterSdkPath =
|
||||
run {
|
||||
val properties = java.util.Properties()
|
||||
file("local.properties").inputStream().use { properties.load(it) }
|
||||
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||
flutterSdkPath
|
||||
}
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.11.2" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.2.21" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
@@ -0,0 +1,92 @@
|
||||
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION AND CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
After Width: | Height: | Size: 14 KiB |
@@ -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)
|
||||
}
|
||||
}
|
||||