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>
This commit is contained in:
2026-08-13 18:34:25 +08:00
parent 61c3fa1df9
commit 9dfa06ffee
3785 changed files with 1085458 additions and 2 deletions
+4
View File
@@ -0,0 +1,4 @@
{
"BUZZ_RELAY_URL": "http://localhost:3000",
"BUZZ_DEV_PUBKEY": "<your-hex-pubkey-here>"
}
+48
View File
@@ -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
+33
View File
@@ -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'
+80
View File
@@ -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))
+100
View File
@@ -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/`
+11
View File
@@ -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
+15
View File
@@ -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
+176
View File
@@ -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)
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 407 B

Binary file not shown.

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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

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)
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 827 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 857 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 B

+24
View File
@@ -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)
}
+2
View File
@@ -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
+26
View File
@@ -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")
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+92
View File
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+36
View File
@@ -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
+24
View File
@@ -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>
+21
View File
@@ -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"
+11
View File
@@ -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"
+42
View File
@@ -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
+111
View File
@@ -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
+776
View File
@@ -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>
+10
View File
@@ -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>
+586
View File
@@ -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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 918 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

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)
}
}

Some files were not shown because too many files have changed in this diff Show More